feat(components): 添加卷帘效果地图组件
- 新增 WipeEffectMap 组件实现卷帘效果- 在 App.vue 中添加卷帘效果模式切换按钮 - 修改 TiandituMap 和 SplitScreenMap组件,统一控制栏样式 - 新增 wipeEffect.js 工具类实现卷帘效果逻辑
This commit is contained in:
parent
66f9ec56e0
commit
6f131047d3
13
src/App.vue
13
src/App.vue
|
@ -16,11 +16,19 @@
|
||||||
>
|
>
|
||||||
分屏对比
|
分屏对比
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
@click="switchMapMode('wipe')"
|
||||||
|
:class="{ active: currentMode === 'wipe' }"
|
||||||
|
class="mode-btn"
|
||||||
|
>
|
||||||
|
卷帘效果
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 根据模式显示不同的地图组件 -->
|
<!-- 根据模式显示不同的地图组件 -->
|
||||||
<TiandituMap v-if="currentMode === 'normal'" />
|
<TiandituMap v-if="currentMode === 'normal'" />
|
||||||
<SplitScreenMap v-if="currentMode === 'split'" />
|
<SplitScreenMap v-if="currentMode === 'split'" />
|
||||||
|
<WipeEffectMap v-if="currentMode === 'wipe'" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -28,6 +36,7 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import TiandituMap from './components/TiandituMap.vue'
|
import TiandituMap from './components/TiandituMap.vue'
|
||||||
import SplitScreenMap from './components/SplitScreenMap.vue'
|
import SplitScreenMap from './components/SplitScreenMap.vue'
|
||||||
|
import WipeEffectMap from './components/WipeEffectMap.vue'
|
||||||
|
|
||||||
const currentMode = ref('normal')
|
const currentMode = ref('normal')
|
||||||
|
|
||||||
|
@ -61,7 +70,7 @@ html, body {
|
||||||
.map-mode-selector {
|
.map-mode-selector {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
right: 125px;
|
right: 120px;
|
||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
|
@ -106,7 +115,7 @@ html, body {
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.map-mode-selector {
|
.map-mode-selector {
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 125px;
|
right: 120px;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import View from 'ol/View'
|
||||||
import { fromLonLat } from 'ol/proj'
|
import { fromLonLat } from 'ol/proj'
|
||||||
import { TIANDITU_CONFIG } from '../config/tianditu.js'
|
import { TIANDITU_CONFIG } from '../config/tianditu.js'
|
||||||
import { createVectorLayers, createSatelliteLayers } from '../utils/tiandituLayers.js'
|
import { createVectorLayers, createSatelliteLayers } from '../utils/tiandituLayers.js'
|
||||||
|
import { defaults } from 'ol/control';
|
||||||
|
|
||||||
const mapDiv = ref()
|
const mapDiv = ref()
|
||||||
let map = null
|
let map = null
|
||||||
|
@ -36,6 +37,12 @@ const currentLayer = ref('vector')
|
||||||
const initMap = () => {
|
const initMap = () => {
|
||||||
// 创建初始图层(矢量地图)
|
// 创建初始图层(矢量地图)
|
||||||
const vectorLayers = createVectorLayers()
|
const vectorLayers = createVectorLayers()
|
||||||
|
|
||||||
|
const controls = defaults({
|
||||||
|
attribution: false,
|
||||||
|
zoom: false,
|
||||||
|
rotate: false
|
||||||
|
});
|
||||||
|
|
||||||
map = new Map({
|
map = new Map({
|
||||||
target: mapDiv.value,
|
target: mapDiv.value,
|
||||||
|
@ -43,7 +50,8 @@ const initMap = () => {
|
||||||
view: new View({
|
view: new View({
|
||||||
center: fromLonLat(TIANDITU_CONFIG.DEFAULT_CENTER),
|
center: fromLonLat(TIANDITU_CONFIG.DEFAULT_CENTER),
|
||||||
zoom: TIANDITU_CONFIG.DEFAULT_ZOOM
|
zoom: TIANDITU_CONFIG.DEFAULT_ZOOM
|
||||||
})
|
}),
|
||||||
|
controls: controls
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,318 @@
|
||||||
|
<template>
|
||||||
|
<div class="wipe-effect-container">
|
||||||
|
<div ref="mapDiv" class="wipe-map-view"></div>
|
||||||
|
<div class="wipe-controls">
|
||||||
|
<div class="control-panel">
|
||||||
|
<h3>卷帘效果对比</h3>
|
||||||
|
<div class="control-buttons">
|
||||||
|
<button @click="toggleWipeEffect" class="control-btn" :class="{ active: isWipeActive }">
|
||||||
|
{{ isWipeActive ? '关闭卷帘' : '开启卷帘' }}
|
||||||
|
</button>
|
||||||
|
<button @click="resetWipe" class="control-btn">
|
||||||
|
重置位置
|
||||||
|
</button>
|
||||||
|
<button @click="toggleDirection" class="control-btn">
|
||||||
|
{{ isVertical ? '水平卷帘' : '垂直卷帘' }}
|
||||||
|
</button>
|
||||||
|
<button @click="animateToPosition(0)" class="control-btn preset-btn">
|
||||||
|
显示基础图层
|
||||||
|
</button>
|
||||||
|
<button @click="animateToPosition(100)" class="control-btn preset-btn">
|
||||||
|
显示覆盖图层
|
||||||
|
</button>
|
||||||
|
<button @click="wiggleAnimation" class="control-btn animation-btn">
|
||||||
|
摆动演示
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="map-info">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">基础图层:</span>
|
||||||
|
<span class="value">矢量地图</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">覆盖图层:</span>
|
||||||
|
<span class="value">影像地图</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">卷帘位置:</span>
|
||||||
|
<span class="value">{{ Math.round(currentPosition) }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">卷帘方向:</span>
|
||||||
|
<span class="value">{{ isVertical ? '垂直' : '水平' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">状态:</span>
|
||||||
|
<span class="value" :class="{ active: isWipeActive }">
|
||||||
|
{{ isWipeActive ? '激活' : '停用' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
|
import Map from 'ol/Map'
|
||||||
|
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 { WipeEffectManager, WipeAnimator } from '../utils/wipeEffect.js'
|
||||||
|
import { defaults } from 'ol/control';
|
||||||
|
|
||||||
|
const mapDiv = ref()
|
||||||
|
let map = null
|
||||||
|
let wipeManager = null
|
||||||
|
let wipeAnimator = null
|
||||||
|
const isWipeActive = ref(false)
|
||||||
|
const isVertical = ref(true)
|
||||||
|
const currentPosition = ref(50)
|
||||||
|
|
||||||
|
// 初始化地图
|
||||||
|
const initMap = () => {
|
||||||
|
const controls = defaults({
|
||||||
|
attribution: false,
|
||||||
|
zoom: false,
|
||||||
|
rotate: false
|
||||||
|
});
|
||||||
|
map = new Map({
|
||||||
|
target: mapDiv.value,
|
||||||
|
view: new View({
|
||||||
|
center: fromLonLat(TIANDITU_CONFIG.DEFAULT_CENTER),
|
||||||
|
zoom: TIANDITU_CONFIG.DEFAULT_ZOOM
|
||||||
|
}),
|
||||||
|
controls: controls,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建卷帘管理器
|
||||||
|
wipeManager = new WipeEffectManager(map, {
|
||||||
|
direction: 'vertical',
|
||||||
|
position: 50
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建卷帘动画器
|
||||||
|
wipeAnimator = new WipeAnimator(wipeManager)
|
||||||
|
|
||||||
|
// 创建图层
|
||||||
|
const vectorLayers = createVectorLayers()
|
||||||
|
const satelliteLayers = createSatelliteLayers()
|
||||||
|
|
||||||
|
// 设置图层(影像作为覆盖,矢量作为基础)
|
||||||
|
// 基础图层(左侧/上方显示):矢量地图
|
||||||
|
// 覆盖图层(右侧/下方显示):影像地图
|
||||||
|
wipeManager.setLayers(satelliteLayers, vectorLayers)
|
||||||
|
|
||||||
|
// 监听位置变化
|
||||||
|
const updatePosition = () => {
|
||||||
|
currentPosition.value = wipeManager.options.position
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定期更新位置显示
|
||||||
|
setInterval(updatePosition, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换卷帘效果
|
||||||
|
const toggleWipeEffect = () => {
|
||||||
|
if (isWipeActive.value) {
|
||||||
|
wipeManager.deactivate()
|
||||||
|
isWipeActive.value = false
|
||||||
|
} else {
|
||||||
|
wipeManager.activate()
|
||||||
|
isWipeActive.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置卷帘位置
|
||||||
|
const resetWipe = async () => {
|
||||||
|
if (wipeAnimator) {
|
||||||
|
await wipeAnimator.animateToPosition(50, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换卷帘方向
|
||||||
|
const toggleDirection = () => {
|
||||||
|
isVertical.value = !isVertical.value
|
||||||
|
if (wipeManager) {
|
||||||
|
wipeManager.toggleDirection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动画到指定位置
|
||||||
|
const animateToPosition = async (position) => {
|
||||||
|
if (wipeAnimator) {
|
||||||
|
await wipeAnimator.animateToPosition(position, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 摆动动画演示
|
||||||
|
const wiggleAnimation = async () => {
|
||||||
|
if (wipeAnimator) {
|
||||||
|
await wipeAnimator.wiggle(15, 2, 1500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initMap()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (wipeManager) {
|
||||||
|
wipeManager.destroy()
|
||||||
|
}
|
||||||
|
if (map) {
|
||||||
|
map.setTarget(undefined)
|
||||||
|
map = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wipe-effect-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wipe-map-view {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wipe-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 280px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 2px solid #007ac8;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: linear-gradient(135deg, #007ac8, #005a96);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 122, 200, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #005a96, #004070);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 122, 200, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.active {
|
||||||
|
background: linear-gradient(135deg, #28a745, #20c997);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.active:hover {
|
||||||
|
background: linear-gradient(135deg, #1e7e34, #17a2b8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn {
|
||||||
|
background: linear-gradient(135deg, #6f42c1, #e83e8c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #5a32a3, #d63384);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-btn {
|
||||||
|
background: linear-gradient(135deg, #ffc107, #fd7e14);
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #e0a800, #dc3545);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-info {
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
background: #f0f8ff;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #007ac8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value.active {
|
||||||
|
background: #d4edda;
|
||||||
|
border-color: #28a745;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wipe-controls {
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
padding: 15px;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,6 +6,7 @@ import View from 'ol/View'
|
||||||
import TileLayer from 'ol/layer/Tile'
|
import TileLayer from 'ol/layer/Tile'
|
||||||
import XYZ from 'ol/source/XYZ'
|
import XYZ from 'ol/source/XYZ'
|
||||||
import { fromLonLat } from 'ol/proj'
|
import { fromLonLat } from 'ol/proj'
|
||||||
|
import { defaults } from 'ol/control';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分屏地图管理器
|
* 分屏地图管理器
|
||||||
|
@ -152,13 +153,18 @@ export class SplitScreenManager {
|
||||||
* 创建地图
|
* 创建地图
|
||||||
*/
|
*/
|
||||||
createMap(container, options) {
|
createMap(container, options) {
|
||||||
|
const controls = defaults({
|
||||||
|
attribution: false,
|
||||||
|
zoom: false,
|
||||||
|
rotate: false
|
||||||
|
});
|
||||||
return new Map({
|
return new Map({
|
||||||
target: container,
|
target: container,
|
||||||
view: new View({
|
view: new View({
|
||||||
center: fromLonLat(options.center),
|
center: fromLonLat(options.center),
|
||||||
zoom: options.zoom
|
zoom: options.zoom
|
||||||
}),
|
}),
|
||||||
controls: []
|
controls: controls
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue