|
|
|
@ -1,7 +1,7 @@ |
|
|
|
/** |
|
|
|
* 动画服务类 |
|
|
|
*/ |
|
|
|
import type { Map } from 'ol' |
|
|
|
import type { Map as OpenLayersMap } from 'ol' |
|
|
|
|
|
|
|
import { Vector as VectorLayer } from 'ol/layer' |
|
|
|
import { Vector as VectorSource } from 'ol/source' |
|
|
|
@ -13,18 +13,51 @@ import type { MarkerData } from '../types/map.types' |
|
|
|
|
|
|
|
import { ANIMATION_CONFIG } from '../constants/map.constants' |
|
|
|
|
|
|
|
// 全局复用:透明填充
|
|
|
|
const TRANSPARENT_FILL = new Fill({ color: 'transparent' }) |
|
|
|
// 预生成 Stroke 缓存(按波纹层级)
|
|
|
|
const STROKE_CACHE: Record<number, Stroke> = {} |
|
|
|
for (let i = 0; i < ANIMATION_CONFIG.rippleCount; i++) { |
|
|
|
const width = Math.max(1, 3 - i * 0.4) |
|
|
|
STROKE_CACHE[i] = new Stroke({ width }) |
|
|
|
} |
|
|
|
|
|
|
|
// 预生成 Style 模板(避免每帧 new)
|
|
|
|
const createRippleStyle = ( |
|
|
|
radius: number, |
|
|
|
stroke: Stroke, |
|
|
|
opacity: number, |
|
|
|
color: string |
|
|
|
): Style => { |
|
|
|
// 注意:color 必须带 alpha,如 #ff0000aa
|
|
|
|
const strokeColor = |
|
|
|
color.slice(0, 7) + |
|
|
|
Math.floor(opacity * 255) |
|
|
|
.toString(16) |
|
|
|
.padStart(2, '0') |
|
|
|
stroke.setColor(strokeColor) |
|
|
|
return new Style({ |
|
|
|
image: new Circle({ |
|
|
|
radius, |
|
|
|
fill: TRANSPARENT_FILL, |
|
|
|
stroke |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
export class AnimationService { |
|
|
|
private rippleLayer: VectorLayer<VectorSource> | null = null |
|
|
|
|
|
|
|
private animationTimer: number | null = null |
|
|
|
private map: Map | null = null |
|
|
|
constructor(map: Map) { |
|
|
|
private map: OpenLayersMap | null = null |
|
|
|
constructor(map: OpenLayersMap) { |
|
|
|
this.map = map |
|
|
|
this.createRippleLayer() |
|
|
|
} |
|
|
|
private transparentFill: Fill = new Fill({ |
|
|
|
color: 'transparent' |
|
|
|
}) |
|
|
|
private statusColor: Set<string> = new Set() |
|
|
|
private statusColorStyleMap: Map<string, Style[]> = new Map() |
|
|
|
|
|
|
|
private globalAnimationStartTime: number = Date.now() |
|
|
|
/** |
|
|
|
* 创建波纹图层 |
|
|
|
*/ |
|
|
|
@ -35,49 +68,60 @@ export class AnimationService { |
|
|
|
source: source, |
|
|
|
zIndex: 1, |
|
|
|
style: (feature) => { |
|
|
|
const startTime = feature.get('animationStart') |
|
|
|
// const startTime = feature.get('animationStart')
|
|
|
|
// console.time('animation style')
|
|
|
|
const color = feature.get('rippleColor') |
|
|
|
const elapsed = (Date.now() - startTime) / 1000 // 秒
|
|
|
|
|
|
|
|
// 创建多个波纹圈
|
|
|
|
const styles: Style[] = [] |
|
|
|
|
|
|
|
for (let i = 0; i < ANIMATION_CONFIG.rippleCount; i++) { |
|
|
|
const phase = (elapsed + i * ANIMATION_CONFIG.phaseOffset) % ANIMATION_CONFIG.duration |
|
|
|
const progress = phase / ANIMATION_CONFIG.duration // 0-1的进度
|
|
|
|
|
|
|
|
// 使用缓动函数使动画更平滑
|
|
|
|
const easeProgress = 1 - Math.pow(1 - progress, 3) // ease-out cubic
|
|
|
|
|
|
|
|
// 计算半径和透明度
|
|
|
|
const radius = |
|
|
|
ANIMATION_CONFIG.minRadius + |
|
|
|
easeProgress * (ANIMATION_CONFIG.maxRadius - ANIMATION_CONFIG.minRadius) |
|
|
|
const opacity = Math.max(0, 1 - easeProgress) // 1到0的透明度
|
|
|
|
|
|
|
|
if (opacity > ANIMATION_CONFIG.minOpacity) { |
|
|
|
// 计算颜色透明度
|
|
|
|
const alpha = Math.floor(opacity * 255) |
|
|
|
.toString(16) |
|
|
|
.padStart(2, '0') |
|
|
|
const strokeColor = color + alpha |
|
|
|
|
|
|
|
styles.push( |
|
|
|
new Style({ |
|
|
|
image: new Circle({ |
|
|
|
radius: radius, |
|
|
|
fill: this.transparentFill, |
|
|
|
stroke: new Stroke({ |
|
|
|
color: strokeColor, |
|
|
|
width: Math.max(1, 3 - i * 0.4) // 动态调整线宽
|
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
) |
|
|
|
} |
|
|
|
const styles = this.statusColorStyleMap.get(color) || [] |
|
|
|
if (styles.length === 0) { |
|
|
|
return [] |
|
|
|
} |
|
|
|
|
|
|
|
// console.timeEnd('animation style')
|
|
|
|
return styles |
|
|
|
|
|
|
|
// const elapsed = (Date.now() - this.globalAnimationStartTime) / 1000 // 秒
|
|
|
|
|
|
|
|
// // 创建多个波纹圈
|
|
|
|
// const styles: Style[] = []
|
|
|
|
|
|
|
|
// for (let i = 0; i < ANIMATION_CONFIG.rippleCount; i++) {
|
|
|
|
// const phase = (elapsed + i * ANIMATION_CONFIG.phaseOffset) % ANIMATION_CONFIG.duration
|
|
|
|
// const progress = phase / ANIMATION_CONFIG.duration // 0-1的进度
|
|
|
|
|
|
|
|
// // 使用缓动函数使动画更平滑
|
|
|
|
// const easeProgress = 1 - Math.pow(1 - progress, 3) // ease-out cubic
|
|
|
|
|
|
|
|
// // 计算半径和透明度
|
|
|
|
// const radius =
|
|
|
|
// ANIMATION_CONFIG.minRadius +
|
|
|
|
// easeProgress * (ANIMATION_CONFIG.maxRadius - ANIMATION_CONFIG.minRadius)
|
|
|
|
// const opacity = Math.max(0, 1 - easeProgress) // 1到0的透明度
|
|
|
|
|
|
|
|
// if (opacity > ANIMATION_CONFIG.minOpacity) {
|
|
|
|
// // 计算颜色透明度
|
|
|
|
// // const alpha = Math.floor(opacity * 255)
|
|
|
|
// // .toString(16)
|
|
|
|
// // .padStart(2, '0')
|
|
|
|
// // const strokeColor = color + alpha
|
|
|
|
|
|
|
|
// // styles.push(
|
|
|
|
// // new Style({
|
|
|
|
// // image: new Circle({
|
|
|
|
// // radius: radius,
|
|
|
|
// // fill: this.transparentFill,
|
|
|
|
// // stroke: new Stroke({
|
|
|
|
// // color: strokeColor,
|
|
|
|
// // width: Math.max(1, 3 - i * 0.4) // 动态调整线宽
|
|
|
|
// // })
|
|
|
|
// // })
|
|
|
|
// // })
|
|
|
|
// // )
|
|
|
|
// // / 复用 Stroke,动态改颜色
|
|
|
|
// const style = createRippleStyle(radius, STROKE_CACHE[i], opacity, color)
|
|
|
|
// styles.push(style)
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return styles
|
|
|
|
} |
|
|
|
}) |
|
|
|
if (this.map) { |
|
|
|
@ -86,14 +130,21 @@ export class AnimationService { |
|
|
|
} |
|
|
|
|
|
|
|
clear() { |
|
|
|
console.time('animationService clear') |
|
|
|
|
|
|
|
const source = this.rippleLayer?.getSource() |
|
|
|
source?.clear(true) |
|
|
|
console.timeEnd('animationService clear') |
|
|
|
} |
|
|
|
|
|
|
|
addAll(markers: MarkerData[]) { |
|
|
|
this.clear() |
|
|
|
const source = this.rippleLayer?.getSource() |
|
|
|
console.time('animationService addAll') |
|
|
|
this.statusColor.clear() |
|
|
|
this.statusColorStyleMap.clear() |
|
|
|
// this.clear()
|
|
|
|
// const source = this.rippleLayer?.getSource()
|
|
|
|
|
|
|
|
const source = new VectorSource() |
|
|
|
const features: Feature[] = [] |
|
|
|
|
|
|
|
// 为每个标记添加波纹效果
|
|
|
|
@ -104,18 +155,26 @@ export class AnimationService { |
|
|
|
}) |
|
|
|
|
|
|
|
// 设置动画开始时间
|
|
|
|
feature.set('animationStart', Date.now()) |
|
|
|
// feature.set('animationStart', Date.now())
|
|
|
|
feature.set('rippleColor', marker.statusColor) |
|
|
|
features.push(feature) |
|
|
|
if (marker.statusColor) { |
|
|
|
this.statusColor.add(marker.statusColor) |
|
|
|
} |
|
|
|
}) |
|
|
|
source?.addFeatures(features) |
|
|
|
this.rippleLayer?.setSource(source) |
|
|
|
console.timeEnd('animationService addAll') |
|
|
|
} |
|
|
|
|
|
|
|
show() { |
|
|
|
this.globalAnimationStartTime = Date.now() // ✅ 重置动画起点
|
|
|
|
this.rippleLayer?.setVisible(true) |
|
|
|
this.startAnimation() |
|
|
|
} |
|
|
|
hide() { |
|
|
|
console.log('动画暂停') |
|
|
|
|
|
|
|
this.stopAnimation() |
|
|
|
this.rippleLayer?.setVisible(false) |
|
|
|
} |
|
|
|
@ -128,6 +187,32 @@ export class AnimationService { |
|
|
|
|
|
|
|
const animateRipples = (currentTime: number) => { |
|
|
|
if (this.rippleLayer && currentTime - lastUpdateTime >= frameInterval) { |
|
|
|
const elapsed = (Date.now() - this.globalAnimationStartTime) / 1000 // 秒
|
|
|
|
|
|
|
|
for (const color of this.statusColor) { |
|
|
|
// 创建多个波纹圈
|
|
|
|
const styles: Style[] = [] |
|
|
|
for (let i = 0; i < ANIMATION_CONFIG.rippleCount; i++) { |
|
|
|
const phase = (elapsed + i * ANIMATION_CONFIG.phaseOffset) % ANIMATION_CONFIG.duration |
|
|
|
const progress = phase / ANIMATION_CONFIG.duration // 0-1的进度
|
|
|
|
|
|
|
|
// 使用缓动函数使动画更平滑
|
|
|
|
const easeProgress = 1 - Math.pow(1 - progress, 3) // ease-out cubic
|
|
|
|
|
|
|
|
// 计算半径和透明度
|
|
|
|
const radius = |
|
|
|
ANIMATION_CONFIG.minRadius + |
|
|
|
easeProgress * (ANIMATION_CONFIG.maxRadius - ANIMATION_CONFIG.minRadius) |
|
|
|
const opacity = Math.max(0, 1 - easeProgress) // 1到0的透明度
|
|
|
|
|
|
|
|
if (opacity > ANIMATION_CONFIG.minOpacity) { |
|
|
|
const style = createRippleStyle(radius, STROKE_CACHE[i], opacity, color) |
|
|
|
styles.push(style) |
|
|
|
} |
|
|
|
} |
|
|
|
this.statusColorStyleMap.set(color, styles) |
|
|
|
} |
|
|
|
|
|
|
|
this.rippleLayer.getSource()?.changed() |
|
|
|
lastUpdateTime = currentTime |
|
|
|
} |
|
|
|
|