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