You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
7.3 KiB
247 lines
7.3 KiB
/**
|
|
* 动画服务类
|
|
*/
|
|
import type { Map as OpenLayersMap } from 'ol'
|
|
|
|
import { Vector as VectorLayer } from 'ol/layer'
|
|
import { Vector as VectorSource } from 'ol/source'
|
|
import { Feature } from 'ol'
|
|
import { Point } from 'ol/geom'
|
|
import { Style, Circle, Fill, Stroke } from 'ol/style'
|
|
import { fromLonLat } from 'ol/proj'
|
|
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: OpenLayersMap | null = null
|
|
constructor(map: OpenLayersMap) {
|
|
this.map = map
|
|
this.createRippleLayer()
|
|
}
|
|
private statusColor: Set<string> = new Set()
|
|
private statusColorStyleMap: Map<string, Style[]> = new Map()
|
|
|
|
private globalAnimationStartTime: number = Date.now()
|
|
/**
|
|
* 创建波纹图层
|
|
*/
|
|
private createRippleLayer() {
|
|
const source = new VectorSource()
|
|
|
|
this.rippleLayer = new VectorLayer({
|
|
source: source,
|
|
zIndex: 1,
|
|
style: (feature) => {
|
|
// const startTime = feature.get('animationStart')
|
|
// console.time('animation style')
|
|
const color = feature.get('rippleColor')
|
|
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) {
|
|
this.map?.addLayer(this.rippleLayer)
|
|
}
|
|
}
|
|
|
|
clear() {
|
|
console.time('animationService clear')
|
|
|
|
const source = this.rippleLayer?.getSource()
|
|
source?.clear(true)
|
|
console.timeEnd('animationService clear')
|
|
}
|
|
|
|
addAll(markers: MarkerData[]) {
|
|
console.time('animationService addAll')
|
|
this.statusColor.clear()
|
|
this.statusColorStyleMap.clear()
|
|
// this.clear()
|
|
// const source = this.rippleLayer?.getSource()
|
|
|
|
const source = new VectorSource()
|
|
const features: Feature[] = []
|
|
|
|
// 为每个标记添加波纹效果
|
|
markers.forEach((marker) => {
|
|
const feature = new Feature({
|
|
geometry: new Point(fromLonLat(marker.coordinates)),
|
|
markerData: marker
|
|
})
|
|
|
|
// 设置动画开始时间
|
|
// 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)
|
|
}
|
|
/**
|
|
* 启动波纹动画
|
|
*/
|
|
startAnimation(): void {
|
|
let lastUpdateTime = 0
|
|
const frameInterval = 1000 / ANIMATION_CONFIG.targetFPS // 帧间隔
|
|
|
|
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
|
|
}
|
|
this.animationTimer = requestAnimationFrame(animateRipples)
|
|
}
|
|
animateRipples(0)
|
|
}
|
|
|
|
/**
|
|
* 停止波纹动画
|
|
*/
|
|
stopAnimation(): void {
|
|
if (this.animationTimer) {
|
|
cancelAnimationFrame(this.animationTimer)
|
|
this.animationTimer = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 销毁动画服务
|
|
*/
|
|
destroy(): void {
|
|
this.stopAnimation()
|
|
|
|
if (this.rippleLayer && this.map) {
|
|
this.map?.removeLayer(this.rippleLayer)
|
|
}
|
|
|
|
this.rippleLayer = null
|
|
this.map = null
|
|
}
|
|
}
|
|
|