UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

239 lines (203 loc) 6.79 kB
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', } }