@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
239 lines (203 loc) • 6.79 kB
text/typescript
import { KeyValue } from '../types'
import { ObjectExt, Dom } from '../util'
import { Timing, Interp } from '../common'
import { Cell } from './cell'
export class Animation {
protected readonly ids: { [path: string]: number } = {}
protected readonly cache: {
[path: string]: {
startValue: any
targetValue: any
options: Animation.StartOptions<any>
}
} = {}
constructor(protected readonly cell: Cell) {}
get() {
return Object.keys(this.ids)
}
start<T extends Animation.TargetValue>(
path: string | string[],
targetValue: T,
options: Animation.StartOptions<T> = {},
delim = '/',
): () => void {
const startValue = this.cell.getPropByPath<T>(path)
const localOptions = ObjectExt.defaults(options, Animation.defaultOptions)
const timing = this.getTiming(localOptions.timing)
const interpolate = this.getInterp<T>(
localOptions.interp,
startValue,
targetValue,
)
let startTime = 0
const key = Array.isArray(path) ? path.join(delim) : path
const paths = Array.isArray(path) ? path : path.split(delim)
const iterate = () => {
const now = new Date().getTime()
if (startTime === 0) {
startTime = now
}
const elaspe = now - startTime
let progress = elaspe / localOptions.duration
if (progress < 1) {
this.ids[key] = Dom.requestAnimationFrame(iterate)
} else {
progress = 1
}
const currentValue = interpolate(timing(progress)) as T
this.cell.setPropByPath(paths, currentValue)
if (options.progress) {
options.progress({ progress, currentValue, ...this.getArgs<T>(key) })
}
if (progress === 1) {
// TODO: remove in the next major version
this.cell.notify('transition:end', this.getArgs<T>(key))
this.cell.notify('transition:complete', this.getArgs<T>(key))
options.complete && options.complete(this.getArgs<T>(key))
this.cell.notify('transition:finish', this.getArgs<T>(key))
options.finish && options.finish(this.getArgs<T>(key))
this.clean(key)
}
}
setTimeout(() => {
this.stop(path, undefined, delim)
this.cache[key] = { startValue, targetValue, options: localOptions }
this.ids[key] = Dom.requestAnimationFrame(iterate)
// TODO: remove in the next major version
this.cell.notify('transition:begin', this.getArgs<T>(key))
this.cell.notify('transition:start', this.getArgs<T>(key))
options.start && options.start(this.getArgs<T>(key))
}, options.delay)
return this.stop.bind(this, path, delim, options)
}
stop<T extends Animation.TargetValue>(
path: string | string[],
options: Animation.StopOptions<T> = {},
delim = '/',
) {
const paths = Array.isArray(path) ? path : path.split(delim)
Object.keys(this.ids)
.filter((key) =>
ObjectExt.isEqual(paths, key.split(delim).slice(0, paths.length)),
)
.forEach((key) => {
Dom.cancelAnimationFrame(this.ids[key])
const data = this.cache[key]
const commonArgs = this.getArgs<T>(key)
const localOptions = { ...data.options, ...options }
const jumpedToEnd = localOptions.jumpedToEnd
if (jumpedToEnd && data.targetValue != null) {
this.cell.setPropByPath(key, data.targetValue)
this.cell.notify('transition:end', { ...commonArgs })
this.cell.notify('transition:complete', { ...commonArgs })
localOptions.complete && localOptions.complete({ ...commonArgs })
}
const stopArgs = { jumpedToEnd, ...commonArgs }
this.cell.notify('transition:stop', { ...stopArgs })
localOptions.stop && localOptions.stop({ ...stopArgs })
this.cell.notify('transition:finish', { ...commonArgs })
localOptions.finish && localOptions.finish({ ...commonArgs })
this.clean(key)
})
return this
}
private clean(key: string) {
delete this.ids[key]
delete this.cache[key]
}
private getTiming(timing: Timing.Names | Timing.Definition) {
return typeof timing === 'string' ? Timing[timing] : timing
}
private getInterp<T extends Animation.TargetValue>(
interp: Interp.Definition<any> | undefined,
startValue: T,
targetValue: T,
) {
if (interp) {
return interp(startValue, targetValue)
}
if (typeof targetValue === 'number') {
return Interp.number(startValue as number, targetValue)
}
if (typeof targetValue === 'string') {
if (targetValue[0] === '#') {
return Interp.color(startValue as string, targetValue)
}
return Interp.unit(startValue as string, targetValue)
}
return Interp.object(
startValue as KeyValue<number>,
targetValue as KeyValue<number>,
)
}
private getArgs<T extends Animation.TargetValue>(
key: string,
): Animation.CallbackArgs<T> {
const data = this.cache[key]
return {
path: key,
startValue: data.startValue,
targetValue: data.targetValue,
cell: this.cell,
}
}
}
export namespace Animation {
export interface BaseOptions {
delay: number
duration: number
timing: Timing.Names | Timing.Definition
}
export type TargetValue = string | number | KeyValue<number>
export interface CallbackArgs<T> {
cell: Cell
path: string
startValue: T
targetValue: T
}
export interface ProgressArgs<T> extends CallbackArgs<T> {
progress: number
currentValue: T
}
export interface StopArgs<T> extends CallbackArgs<T> {
jumpedToEnd?: boolean
}
export interface StartOptions<T>
extends Partial<BaseOptions>,
StopOptions<T> {
interp?: Interp.Definition<any>
/**
* A function to call when the animation begins.
*/
start?: (options: CallbackArgs<T>) => void
/**
* A function to be called after each step of the animation, only once per
* animated element regardless of the number of animated properties.
*/
progress?: (options: ProgressArgs<T>) => void
}
export interface StopOptions<T> {
/**
* A Boolean indicating whether to complete the animation immediately.
* Defaults to `false`.
*/
jumpedToEnd?: boolean
/**
* A function that is called once the animation completes.
*/
complete?: (options: CallbackArgs<T>) => void
/**
* A function to be called when the animation stops.
*/
stop?: (options: StopArgs<T>) => void
/**
* A function to be called when the animation completes or stops.
*/
finish?: (options: CallbackArgs<T>) => void
}
export const defaultOptions: BaseOptions = {
delay: 10,
duration: 100,
timing: 'linear',
}
}