// 地图卷帘效果工具类 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() }) } }