geo/src/utils/wipeEffect.js

474 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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