packages/utils/animation/src/PathAnimation.ts (144 lines of code) (raw):
/**
* Copyright (C) 2021 Alibaba Group Holding Limited
* All rights reserved.
*/
import { Polaris } from '@polaris.gl/base'
import { smoothstep } from '@polaris.gl/utils-easing'
import { Track } from 'ani-timeline'
export type Path = {
cameraStates: string
duration: number
delay?: number
easing?: 'linear' | 'smooth'
}
export interface PathAnimationConfig {
loop?: boolean
}
export const defaultConfig = {
loop: false,
}
export class PathAnimation {
polaris: Polaris
paths: Path[]
loop = false
private declare _managerTrack: Track | undefined
private declare _currPathIndex: number
private declare _currTrackStopFn: { (): void } | undefined
private _stoped = true
private _paused = true
constructor(polarisInstance: Polaris, config: PathAnimationConfig) {
this.polaris = polarisInstance
const _config = {
...defaultConfig,
...config,
}
this.loop = _config.loop
this.paths = []
}
addPath(path: Path) {
this.paths.push(path)
}
setPaths(paths: Path[]) {
this.paths = paths
}
canPlay() {
return this.paths.length >= 2
}
play() {
if (!this.canPlay()) {
console.warn('Polaris::PathAnimation - paths length is less than 2, cannot play animation. ')
return
}
if (this._stoped) {
this._startAnim()
return
}
if (this._paused) {
this._startAnim(true)
return
}
}
pause() {
this._paused = true
if (this._currTrackStopFn) {
this._currTrackStopFn()
}
if (this._managerTrack) {
this._managerTrack.alive = false
this._managerTrack = undefined
}
}
stop() {
this._stoped = true
if (this._currTrackStopFn) {
this._currTrackStopFn()
}
if (this._managerTrack) {
this._managerTrack.alive = false
this._managerTrack = undefined
}
this._currPathIndex = 0
}
clear() {
this.stop()
this.paths = []
}
/**
* Handle the loop behavior of animations
*/
private _startAnim(resume = false) {
// Recurrsion for loop animation
const onAnimFinished = () => {
this._stoped = true
}
if (resume) {
// Start from paused index
this._playAnims(this._currPathIndex, onAnimFinished)
} else {
// Start from first path
this.polaris.setStatesCode(this.paths[0].cameraStates, 0, {
onEnd: () => {
this._playAnims(0, onAnimFinished)
},
})
}
if (this.loop && !this._managerTrack) {
this._managerTrack = this.polaris.timeline.addTrack({
id: 'PathAnimation Loop Manager',
duration: Infinity,
onUpdate: () => {
if (!this._stoped) return
this._startAnim()
},
})
}
this._stoped = false
this._paused = false
}
/**
* The actual animation function, play from certain path index to the end.
* @param startIndex
* @param onFinished
* @returns
*/
private _playAnims(startIndex: number, onFinished?: () => void) {
if (this._stoped) {
return
}
// All path animations were completed
if (startIndex >= this.paths.length - 1) {
onFinished && onFinished()
return
}
const pathStart = this.paths[startIndex]
const pathNext = this.paths[startIndex + 1]
const { duration, delay, easing } = pathStart
let easingFn
if (easing) {
switch (easing) {
case 'smooth':
easingFn = (p) => smoothstep(0, 1, p)
break
// case 'quadric':
// easing = undefined
// break
case 'linear':
default:
easingFn = (p) => p
}
}
this._currTrackStopFn = this.polaris.setStatesCode(pathNext.cameraStates, duration, {
onEnd: () => {
this._currTrackStopFn = undefined
if (this._stoped || this._paused) {
return
}
// Using Timeline.setTimeout() to handle delay for next path
this.polaris.timeline.setTimeout(() => {
this._playAnims(startIndex + 1, onFinished)
}, delay ?? 0)
},
easing: easingFn,
})
this._currPathIndex = startIndex
}
}