diff --git a/src/App.vue b/src/App.vue index b0b8d1b..3e4c363 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,11 +16,19 @@ > 分屏对比 + + @@ -28,6 +36,7 @@ import { ref } from 'vue' import TiandituMap from './components/TiandituMap.vue' import SplitScreenMap from './components/SplitScreenMap.vue' +import WipeEffectMap from './components/WipeEffectMap.vue' const currentMode = ref('normal') @@ -61,7 +70,7 @@ html, body { .map-mode-selector { position: absolute; top: 20px; - right: 125px; + right: 120px; z-index: 2000; display: flex; gap: 5px; @@ -106,7 +115,7 @@ html, body { @media (max-width: 768px) { .map-mode-selector { top: 10px; - right: 125px; + right: 120px; left: 10px; justify-content: center; } diff --git a/src/components/TiandituMap.vue b/src/components/TiandituMap.vue index 34acde4..f5cde87 100644 --- a/src/components/TiandituMap.vue +++ b/src/components/TiandituMap.vue @@ -27,6 +27,7 @@ import View from 'ol/View' import { fromLonLat } from 'ol/proj' import { TIANDITU_CONFIG } from '../config/tianditu.js' import { createVectorLayers, createSatelliteLayers } from '../utils/tiandituLayers.js' +import { defaults } from 'ol/control'; const mapDiv = ref() let map = null @@ -36,6 +37,12 @@ const currentLayer = ref('vector') const initMap = () => { // 创建初始图层(矢量地图) const vectorLayers = createVectorLayers() + + const controls = defaults({ + attribution: false, + zoom: false, + rotate: false + }); map = new Map({ target: mapDiv.value, @@ -43,7 +50,8 @@ const initMap = () => { view: new View({ center: fromLonLat(TIANDITU_CONFIG.DEFAULT_CENTER), zoom: TIANDITU_CONFIG.DEFAULT_ZOOM - }) + }), + controls: controls }) } diff --git a/src/components/WipeEffectMap.vue b/src/components/WipeEffectMap.vue new file mode 100644 index 0000000..2781a2c --- /dev/null +++ b/src/components/WipeEffectMap.vue @@ -0,0 +1,318 @@ + + + + + \ No newline at end of file diff --git a/src/utils/splitScreen.js b/src/utils/splitScreen.js index 0e7bcf3..da8820f 100644 --- a/src/utils/splitScreen.js +++ b/src/utils/splitScreen.js @@ -6,6 +6,7 @@ import View from 'ol/View' import TileLayer from 'ol/layer/Tile' import XYZ from 'ol/source/XYZ' import { fromLonLat } from 'ol/proj' +import { defaults } from 'ol/control'; /** * 分屏地图管理器 @@ -152,13 +153,18 @@ export class SplitScreenManager { * 创建地图 */ createMap(container, options) { + const controls = defaults({ + attribution: false, + zoom: false, + rotate: false + }); return new Map({ target: container, view: new View({ center: fromLonLat(options.center), zoom: options.zoom }), - controls: [] + controls: controls }) } diff --git a/src/utils/wipeEffect.js b/src/utils/wipeEffect.js new file mode 100644 index 0000000..443ff54 --- /dev/null +++ b/src/utils/wipeEffect.js @@ -0,0 +1,474 @@ +// 地图卷帘效果工具类 +import { unByKey } from 'ol/Observable' + +/** + * 卷帘效果管理器 + */ +export class WipeEffectManager { + constructor(map, options = {}) { + this.map = map + this.options = { + direction: 'vertical', // 'vertical' | 'horizontal' + position: 50, // 初始位置 (0-100) + ...options + } + + this.isActive = false + this.wipeElement = null + this.overlayLayers = [] + this.baseLayers = [] + this.eventKeys = [] + this.prerenderKeys = [] + + this.init() + } + + /** + * 初始化卷帘效果 + */ + init() { + this.createWipeElement() + this.addDragFunctionality() + } + + /** + * 创建卷帘元素 + */ + createWipeElement() { + this.wipeElement = document.createElement('div') + this.wipeElement.className = 'wipe-effect' + this.wipeElement.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 1000; + ` + + // 创建卷帘遮罩 + const mask = document.createElement('div') + mask.className = 'wipe-mask' + mask.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + pointer-events: none; + ` + + // 创建卷帘线 + const line = document.createElement('div') + line.className = 'wipe-line' + line.style.cssText = ` + position: absolute; + background: #007ac8; + box-shadow: 0 0 10px rgba(0, 122, 200, 0.5); + pointer-events: auto; + cursor: ${this.options.direction === 'vertical' ? 'col-resize' : 'row-resize'}; + z-index: 1001; + ` + + // 先添加到DOM,再更新位置 + this.wipeElement.appendChild(mask) + this.wipeElement.appendChild(line) + + // 添加到地图容器 + const mapElement = this.map.getViewport() + mapElement.appendChild(this.wipeElement) + + // 现在可以安全地更新位置 + this.updateWipePosition() + } + + /** + * 更新卷帘位置 + */ + updateWipePosition() { + if (!this.wipeElement) return + + const line = this.wipeElement.querySelector('.wipe-line') + const mask = this.wipeElement.querySelector('.wipe-mask') + + // 确保元素存在 + if (!line || !mask) { + console.warn('Wipe elements not found, skipping position update') + return + } + + if (this.options.direction === 'vertical') { + // 垂直卷帘 + line.style.left = `${this.options.position}%` + line.style.top = '0' + line.style.width = '4px' + line.style.height = '100%' + + mask.style.clipPath = `polygon(0 0, ${this.options.position}% 0, ${this.options.position}% 100%, 0 100%)` + } else { + // 水平卷帘 + line.style.top = `${this.options.position}%` + line.style.left = '0' + line.style.width = '100%' + line.style.height = '4px' + + mask.style.clipPath = `polygon(0 0, 100% 0, 100% ${this.options.position}%, 0 ${this.options.position}%)` + } + } + + /** + * 添加拖拽功能 + */ + addDragFunctionality() { + let isDragging = false + let startX = 0 + let startY = 0 + let startPosition = 0 + + const startDrag = (e) => { + isDragging = true + startX = e.clientX + startY = e.clientY + startPosition = this.options.position + document.body.style.cursor = this.options.direction === 'vertical' ? 'col-resize' : 'row-resize' + document.addEventListener('mousemove', drag) + document.addEventListener('mouseup', stopDrag) + e.preventDefault() + } + + const drag = (e) => { + if (!isDragging) return + + if (this.options.direction === 'vertical') { + // 垂直卷帘 - 水平拖拽 + const deltaX = e.clientX - startX + const containerWidth = this.wipeElement.offsetWidth + const newPosition = Math.max(0, Math.min(100, startPosition + (deltaX / containerWidth) * 100)) + this.setPosition(newPosition) + } else { + // 水平卷帘 - 垂直拖拽 + const deltaY = e.clientY - startY + const containerHeight = this.wipeElement.offsetHeight + const newPosition = Math.max(0, Math.min(100, startPosition + (deltaY / containerHeight) * 100)) + this.setPosition(newPosition) + } + } + + const stopDrag = () => { + isDragging = false + document.body.style.cursor = '' + document.removeEventListener('mousemove', drag) + document.removeEventListener('mouseup', stopDrag) + } + + // 延迟绑定事件,确保DOM元素已创建 + setTimeout(() => { + const line = this.wipeElement?.querySelector('.wipe-line') + if (line) { + line.addEventListener('mousedown', startDrag) + } else { + console.warn('Wipe line element not found for drag functionality') + } + }, 0) + } + + /** + * 设置图层 + * @param {Array} overlayLayers - 覆盖图层(显示在卷帘上方) + * @param {Array} baseLayers - 基础图层(显示在卷帘下方) + */ + setLayers(overlayLayers = [], baseLayers = []) { + // 清理之前的渲染事件 + this.clearRenderEvents() + + this.overlayLayers = overlayLayers + this.baseLayers = baseLayers + + // 清空现有图层 + this.map.getLayers().clear() + + // 添加基础图层 + baseLayers.forEach(layer => { + this.map.addLayer(layer) + }) + + // 添加覆盖图层 + overlayLayers.forEach(layer => { + this.map.addLayer(layer) + }) + + // 设置渲染事件 + this.setupRenderEvents() + } + + /** + * 设置渲染事件 + */ + setupRenderEvents() { + if (!this.isActive) return + + // 为覆盖图层添加预渲染事件 + this.overlayLayers.forEach(layer => { + const prerenderKey = layer.on('prerender', (event) => { + const ctx = event.context + const canvas = ctx.canvas + + ctx.save() + + if (this.options.direction === 'vertical') { + // 垂直卷帘:只显示右侧部分 + const clipX = (canvas.width * this.options.position) / 100 + ctx.beginPath() + ctx.rect(clipX, 0, canvas.width - clipX, canvas.height) + ctx.clip() + } else { + // 水平卷帘:只显示下方部分 + const clipY = (canvas.height * this.options.position) / 100 + ctx.beginPath() + ctx.rect(0, clipY, canvas.width, canvas.height - clipY) + ctx.clip() + } + }) + + const postrenderKey = layer.on('postrender', (event) => { + const ctx = event.context + ctx.restore() + }) + + this.prerenderKeys.push(prerenderKey, postrenderKey) + }) + + // 为基础图层添加预渲染事件 + this.baseLayers.forEach(layer => { + const prerenderKey = layer.on('prerender', (event) => { + const ctx = event.context + const canvas = ctx.canvas + + ctx.save() + + if (this.options.direction === 'vertical') { + // 垂直卷帘:只显示左侧部分 + const clipX = (canvas.width * this.options.position) / 100 + ctx.beginPath() + ctx.rect(0, 0, clipX, canvas.height) + ctx.clip() + } else { + // 水平卷帘:只显示上方部分 + const clipY = (canvas.height * this.options.position) / 100 + ctx.beginPath() + ctx.rect(0, 0, canvas.width, clipY) + ctx.clip() + } + }) + + const postrenderKey = layer.on('postrender', (event) => { + const ctx = event.context + ctx.restore() + }) + + this.prerenderKeys.push(prerenderKey, postrenderKey) + }) + } + + /** + * 清理渲染事件 + */ + clearRenderEvents() { + this.prerenderKeys.forEach(key => { + unByKey(key) + }) + this.prerenderKeys = [] + } + + + + /** + * 设置卷帘位置 + * @param {number} position - 位置 (0-100) + */ + setPosition(position) { + this.options.position = Math.max(0, Math.min(100, position)) + this.updateWipePosition() + + // 触发地图重新渲染以应用新的裁剪区域 + if (this.isActive) { + this.map.render() + } + } + + /** + * 切换卷帘方向 + */ + toggleDirection() { + this.options.direction = this.options.direction === 'vertical' ? 'horizontal' : 'vertical' + + if (this.wipeElement) { + const line = this.wipeElement.querySelector('.wipe-line') + if (line) { + line.style.cursor = this.options.direction === 'vertical' ? 'col-resize' : 'row-resize' + } + this.updateWipePosition() + } + + // 重新设置渲染事件以应用新的方向 + if (this.isActive) { + this.clearRenderEvents() + this.setupRenderEvents() + this.map.render() + } + } + + /** + * 重置卷帘位置 + */ + resetPosition() { + this.setPosition(50) + } + + /** + * 激活卷帘效果 + */ + activate() { + this.isActive = true + if (this.wipeElement) { + this.wipeElement.style.display = 'block' + } + + // 设置渲染事件 + this.setupRenderEvents() + + // 触发重新渲染 + this.map.render() + } + + /** + * 停用卷帘效果 + */ + deactivate() { + this.isActive = false + if (this.wipeElement) { + this.wipeElement.style.display = 'none' + } + + // 清理渲染事件 + this.clearRenderEvents() + + // 触发重新渲染 + this.map.render() + } + + /** + * 销毁卷帘效果 + */ + destroy() { + this.deactivate() + + // 清理渲染事件 + this.clearRenderEvents() + + // 清理事件监听器 + this.eventKeys.forEach(key => { + unByKey(key) + }) + this.eventKeys = [] + + // 移除DOM元素 + if (this.wipeElement && this.wipeElement.parentNode) { + this.wipeElement.parentNode.removeChild(this.wipeElement) + } + + this.wipeElement = null + } +} + +/** + * 卷帘动画器 + */ +export class WipeAnimator { + constructor(manager) { + this.manager = manager + this.isAnimating = false + } + + /** + * 动画到指定位置 + * @param {number} targetPosition - 目标位置 + * @param {number} duration - 动画时长(ms) + */ + async animateToPosition(targetPosition, duration = 1000) { + if (this.isAnimating) { + return false + } + + this.isAnimating = true + const startPosition = this.manager.options.position + const distance = targetPosition - startPosition + const startTime = Date.now() + + return new Promise((resolve) => { + const animate = () => { + const elapsed = Date.now() - startTime + const progress = Math.min(elapsed / duration, 1) + + // 使用缓动函数 + const easeProgress = this.easeInOutCubic(progress) + const currentPosition = startPosition + (distance * easeProgress) + + this.manager.setPosition(currentPosition) + + if (progress < 1) { + requestAnimationFrame(animate) + } else { + this.isAnimating = false + resolve(true) + } + } + + animate() + }) + } + + /** + * 缓动函数 + */ + easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 + } + + /** + * 摆动动画 + */ + async wiggle(amplitude = 10, cycles = 3, duration = 1000) { + if (this.isAnimating) { + return false + } + + this.isAnimating = true + const centerPosition = this.manager.options.position + const startTime = Date.now() + + return new Promise((resolve) => { + const animate = () => { + const elapsed = Date.now() - startTime + const progress = elapsed / duration + + if (progress >= 1) { + this.manager.setPosition(centerPosition) + this.isAnimating = false + resolve(true) + return + } + + // 正弦波摆动 + const wiggleOffset = Math.sin(progress * Math.PI * 2 * cycles) * amplitude * (1 - progress) + this.manager.setPosition(centerPosition + wiggleOffset) + + requestAnimationFrame(animate) + } + + animate() + }) + } +} \ No newline at end of file