refactor: ♻️ add animation elements (#1520)
* refactor: ♻️ rename Svg to SVG * refactor: ♻️ add animation elements
This commit is contained in:
@ -1,14 +1,21 @@
|
||||
import React from 'react'
|
||||
import { Svg } from '@antv/x6-vector'
|
||||
import { SVG } from '@antv/x6-vector'
|
||||
import '../index.less'
|
||||
|
||||
console.log(Svg)
|
||||
|
||||
export default class Example extends React.Component {
|
||||
private container: HTMLDivElement
|
||||
|
||||
componentDidMount() {
|
||||
new Svg().appendTo(this.container)
|
||||
const svg = new SVG()
|
||||
const rect = svg.rect(100, 100).node
|
||||
svg.appendTo(this.container)
|
||||
rect.animate(
|
||||
[{ fill: '#000000' }, { fill: '#0000FF' }, { fill: '#00FFFF' }],
|
||||
{
|
||||
duration: 3000,
|
||||
iterations: Infinity,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
refContainer = (container: HTMLDivElement) => {
|
||||
|
@ -1,541 +0,0 @@
|
||||
import { Entity } from '../../types'
|
||||
import type { When, Options } from '../types'
|
||||
import { Easing } from '../stepper/easing'
|
||||
import { Morpher } from '../morpher/morpher'
|
||||
import { Stepper } from '../stepper/stepper'
|
||||
import { Timeline } from '../scheduler/timeline'
|
||||
import { Controller } from '../stepper/controller'
|
||||
import {
|
||||
History,
|
||||
Executors,
|
||||
PrepareMethod,
|
||||
RunMethod,
|
||||
RetargetMethod,
|
||||
} from './types'
|
||||
import { Util } from './util'
|
||||
|
||||
export class Animator<
|
||||
TAnimator,
|
||||
TOwner extends Animator.Owner = Animator.Owner,
|
||||
> {
|
||||
public readonly id: number
|
||||
public readonly declarative: boolean
|
||||
public done = false
|
||||
protected enabled = true
|
||||
protected reseted = true
|
||||
protected persisted: number | boolean
|
||||
|
||||
protected owner: TOwner
|
||||
protected stepper: Stepper
|
||||
protected timeline: Timeline | null = null
|
||||
|
||||
protected duration: number
|
||||
protected times = 1
|
||||
protected wait = 0
|
||||
protected swing = false
|
||||
protected reversal = false
|
||||
|
||||
protected currentTime = 0
|
||||
protected previousStepTime = 0
|
||||
protected previousStepPosition: number
|
||||
|
||||
protected readonly history: History<TAnimator> = {}
|
||||
protected readonly executors: Executors<TAnimator> = []
|
||||
protected readonly callbacks: {
|
||||
[Key in Animator.EventNames]: any[]
|
||||
} = {
|
||||
start: [],
|
||||
step: [],
|
||||
finished: [],
|
||||
}
|
||||
|
||||
constructor()
|
||||
constructor(duration: number)
|
||||
constructor(stepper: Stepper)
|
||||
constructor(options: number | Stepper = Util.defaults.duration) {
|
||||
this.id = Util.generateId()
|
||||
const opts =
|
||||
typeof options === 'function' ? new Controller(options) : options
|
||||
this.stepper = opts instanceof Controller ? opts : new Easing()
|
||||
this.declarative = opts instanceof Controller
|
||||
this.persisted = this.declarative ? true : 0
|
||||
this.duration = typeof opts === 'number' ? opts : 0
|
||||
}
|
||||
|
||||
active(): boolean
|
||||
active(enabled: boolean): this
|
||||
active(enabled?: boolean) {
|
||||
if (enabled == null) {
|
||||
return this.enabled
|
||||
}
|
||||
|
||||
this.enabled = enabled
|
||||
return this
|
||||
}
|
||||
|
||||
ease(): Stepper
|
||||
ease(stepper: Stepper): this
|
||||
ease(stepper?: Stepper) {
|
||||
if (stepper == null) {
|
||||
return this.stepper
|
||||
}
|
||||
|
||||
this.stepper = stepper
|
||||
return this
|
||||
}
|
||||
|
||||
persist(): number
|
||||
/**
|
||||
* Make this runner persist on the timeline forever (true) or for a specific
|
||||
* time. Usually a runner is deleted after execution to clean up memory.
|
||||
*/
|
||||
persist(dt: number): this
|
||||
persist(forever: boolean): this
|
||||
persist(dtOrForever?: number | boolean) {
|
||||
if (dtOrForever == null) {
|
||||
return this.persisted
|
||||
}
|
||||
this.persisted = dtOrForever
|
||||
return this
|
||||
}
|
||||
|
||||
master(): TOwner
|
||||
master(owner: TOwner): this
|
||||
master(owner?: TOwner) {
|
||||
if (owner == null) {
|
||||
return this.owner
|
||||
}
|
||||
|
||||
this.owner = owner
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
scheduler(): Timeline
|
||||
scheduler(timeline: Timeline | null): this
|
||||
scheduler(timeline?: Timeline | null) {
|
||||
if (typeof timeline === 'undefined') {
|
||||
return this.timeline
|
||||
}
|
||||
this.timeline = timeline
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the runner back to zero time and all animations with it
|
||||
*/
|
||||
reset() {
|
||||
if (this.reseted) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.time(0)
|
||||
this.reseted = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration the runner will run
|
||||
*/
|
||||
quantity() {
|
||||
return this.times * (this.wait + this.duration) - this.wait
|
||||
}
|
||||
|
||||
schedule(delay: number, when: When): this
|
||||
schedule(timeline: Timeline, delay: number, when: When): this
|
||||
schedule(timeline: Timeline | number, delay: When | number, when?: When) {
|
||||
if (typeof timeline === 'number') {
|
||||
when = delay as When // eslint-disable-line
|
||||
delay = timeline // eslint-disable-line
|
||||
timeline = this.timeline! // eslint-disable-line
|
||||
}
|
||||
|
||||
if (timeline == null) {
|
||||
throw Error('Runner cannot be scheduled without timeline')
|
||||
}
|
||||
|
||||
const scheduler = timeline as Timeline
|
||||
|
||||
scheduler.schedule(this, delay as number, when)
|
||||
return this
|
||||
}
|
||||
|
||||
unschedule() {
|
||||
const timeline = this.timeline
|
||||
if (timeline) {
|
||||
timeline.unschedule(this)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
loop(times?: number | true, swing?: boolean, wait?: number): this
|
||||
loop(options: { times?: number | true; swing?: boolean; wait?: number }): this
|
||||
loop(
|
||||
times?:
|
||||
| { times?: number | true; swing?: boolean; wait?: number }
|
||||
| number
|
||||
| true,
|
||||
swing?: boolean,
|
||||
wait?: number,
|
||||
) {
|
||||
const o = typeof times === 'object' ? times : { times, swing, wait }
|
||||
this.times = o.times == null || o.times === true ? Infinity : o.times
|
||||
this.swing = o.swing || false
|
||||
this.wait = o.wait || 0
|
||||
return this
|
||||
}
|
||||
|
||||
reverse(reverse?: boolean) {
|
||||
this.reversal = reverse == null ? !this.reversal : reverse
|
||||
return this
|
||||
}
|
||||
|
||||
time(): number
|
||||
time(time: number): this
|
||||
time(time?: number) {
|
||||
if (time == null) {
|
||||
return this.currentTime
|
||||
}
|
||||
const delta = time - this.currentTime
|
||||
this.step(delta)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps the runner to its finished state.
|
||||
*/
|
||||
finish() {
|
||||
return this.step(Infinity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the runner including the wait times
|
||||
* (between 0 and 1).
|
||||
*/
|
||||
progress(): number
|
||||
/**
|
||||
* Sets the current position of the runner including the wait times
|
||||
* (between 0 and 1).
|
||||
*/
|
||||
progress(p: number): this
|
||||
progress(p?: number) {
|
||||
if (p == null) {
|
||||
return Math.min(1, this.currentTime / this.quantity())
|
||||
}
|
||||
return this.time(p * this.quantity())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current iteration of the runner.
|
||||
*/
|
||||
loops(): number
|
||||
/**
|
||||
* Jump to a specific iteration of the runner.
|
||||
* e.g. 3.5 for 4th loop half way through
|
||||
*/
|
||||
loops(p: number): this
|
||||
loops(p?: number) {
|
||||
const duration = this.duration + this.wait
|
||||
|
||||
if (p == null) {
|
||||
const finishedCount = Math.floor(this.currentTime / duration)
|
||||
const delta = this.currentTime - finishedCount * duration
|
||||
const position = delta / this.duration
|
||||
return Math.min(finishedCount + position, this.times)
|
||||
}
|
||||
|
||||
const whole = Math.floor(p)
|
||||
const partial = p % 1
|
||||
const total = duration * whole + this.duration * partial
|
||||
return this.time(total)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current position of the runner ignoring the wait times
|
||||
* (between 0 and 1).
|
||||
*/
|
||||
position(): number
|
||||
/**
|
||||
* Sets the current position of the runner ignoring the wait times
|
||||
* (between 0 and 1).
|
||||
*/
|
||||
position(p: number): this
|
||||
position(p?: number) {
|
||||
const current = this.currentTime
|
||||
const w = this.wait
|
||||
const t = this.times
|
||||
const s = this.swing
|
||||
const r = this.reversal
|
||||
const d = this.duration
|
||||
|
||||
if (p == null) {
|
||||
/*
|
||||
This function converts a time to a position in the range [0, 1]
|
||||
The full explanation can be found in this desmos demonstration
|
||||
https://www.desmos.com/calculator/u4fbavgche
|
||||
The logic is slightly simplified here because we can use booleans
|
||||
*/
|
||||
|
||||
// Figure out the value without thinking about the start or end time
|
||||
const f = (x: number) => {
|
||||
const swinging = (s ? 1 : 0) * Math.floor((x % (2 * (w + d))) / (w + d))
|
||||
const backwards = +((swinging && !r) || (!swinging && r))
|
||||
const uncliped = ((backwards ? -1 : 1) * (x % (w + d))) / d + backwards
|
||||
const clipped = Math.max(Math.min(uncliped, 1), 0)
|
||||
return clipped
|
||||
}
|
||||
|
||||
// Figure out the value by incorporating the start time
|
||||
const endTime = t * (w + d) - w
|
||||
const position =
|
||||
current <= 0
|
||||
? Math.round(f(1e-5))
|
||||
: current < endTime
|
||||
? f(current)
|
||||
: Math.round(f(endTime - 1e-5))
|
||||
return position
|
||||
}
|
||||
|
||||
const finishedCount = Math.floor(this.loops())
|
||||
const swingForward = s && finishedCount % 2 === 0
|
||||
const forwards = (swingForward && !r) || (r && swingForward)
|
||||
const position = finishedCount + (forwards ? p : 1 - p)
|
||||
return this.loops(position)
|
||||
}
|
||||
|
||||
animate(options: Options): this
|
||||
animate(duration?: number, delay?: number, when?: When): this
|
||||
animate(duration?: Options | number, delay?: number, when?: When) {
|
||||
const options = Util.sanitise(duration, delay, when)
|
||||
const Ctor = this.constructor as new (duration: number) => Animator<
|
||||
TAnimator,
|
||||
TOwner
|
||||
>
|
||||
|
||||
const animator = new Ctor(options.duration)
|
||||
|
||||
if (this.timeline) {
|
||||
animator.scheduler(this.timeline)
|
||||
}
|
||||
|
||||
if (this.owner) {
|
||||
animator.master(this.owner)
|
||||
}
|
||||
|
||||
return animator.loop(options).schedule(options.delay, options.when)
|
||||
}
|
||||
|
||||
delay(delay: number) {
|
||||
return this.animate(0, delay)
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the runner by a certain time.
|
||||
*/
|
||||
step(delta = 16) {
|
||||
if (!this.active()) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.currentTime += delta
|
||||
|
||||
// Figure out if we need to run the stepper in this frame
|
||||
const position = this.position()
|
||||
const running =
|
||||
this.previousStepPosition !== position && this.currentTime >= 0
|
||||
this.previousStepPosition = position
|
||||
|
||||
const quantity = this.quantity()
|
||||
const justStarted = this.previousStepTime <= 0 && this.currentTime > 0
|
||||
const justFinished =
|
||||
this.previousStepTime < quantity && this.currentTime >= quantity
|
||||
|
||||
this.previousStepTime = this.currentTime
|
||||
const callback = (cache: any[]) => {
|
||||
for (let i = 0, l = cache.length; i < l; i += 1) {
|
||||
const handler = cache[i] as Animator.EventHandler<TAnimator>
|
||||
const context = cache[i + 1]
|
||||
if (handler) {
|
||||
const result = handler.call(context, this)
|
||||
if (result === false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (justStarted) {
|
||||
if (callback(this.callbacks.start) === false) {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
this.reseted = false
|
||||
|
||||
// Work out if the runner is finished set the done flag here so animations
|
||||
// know, that they are running in the last step (this is good for
|
||||
// transformations which can be merged)
|
||||
const declared = this.declarative
|
||||
|
||||
this.done = !declared && !justFinished && this.currentTime >= quantity
|
||||
|
||||
let converged = false
|
||||
if (running || declared) {
|
||||
this.prepare(running)
|
||||
converged = this.run(declared ? delta : position)
|
||||
if (callback(this.callbacks.step) === false) {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// correct the done flag here
|
||||
// declaritive animations itself know when they converged
|
||||
this.done = this.done || (converged && declared)
|
||||
|
||||
if (justFinished) {
|
||||
if (callback(this.callbacks.finished) === false) {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
on(
|
||||
event: Animator.EventNames,
|
||||
callback: Animator.EventHandler<TAnimator>,
|
||||
context?: Entity,
|
||||
) {
|
||||
const cache = this.callbacks[event]
|
||||
cache.push(callback, context)
|
||||
return this
|
||||
}
|
||||
|
||||
off(
|
||||
event: Animator.EventNames,
|
||||
callback?: Animator.EventHandler<TAnimator>,
|
||||
context?: Entity,
|
||||
) {
|
||||
const cache = this.callbacks[event]
|
||||
if (callback == null && context == null) {
|
||||
this.callbacks[event] = []
|
||||
} else {
|
||||
for (let i = cache.length - 1; i >= 0; i -= 2) {
|
||||
if (
|
||||
(callback == null || cache[i - 1] === callback) &&
|
||||
(context == null || cache[i] === context)
|
||||
) {
|
||||
cache.splice(i - 1, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
protected queue<TTarget, TExtra = any>(
|
||||
prepare?: PrepareMethod<TAnimator> | null,
|
||||
run?: RunMethod<TAnimator> | null,
|
||||
retarget?: RetargetMethod<TAnimator, TTarget, TExtra> | null,
|
||||
isTransform?: boolean,
|
||||
) {
|
||||
this.executors.push({
|
||||
isTransform,
|
||||
retarget,
|
||||
prepare: prepare || (() => undefined),
|
||||
run: run || (() => undefined),
|
||||
ready: false,
|
||||
finished: false,
|
||||
})
|
||||
|
||||
if (this.timeline) {
|
||||
this.timeline.peek()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
protected prepare(running: boolean) {
|
||||
if (running || this.declarative) {
|
||||
for (let i = 0, l = this.executors.length; i < l; i += 1) {
|
||||
const exe = this.executors[i]
|
||||
const needInit = this.declarative || (!exe.ready && running)
|
||||
if (needInit && !exe.finished) {
|
||||
exe.prepare.call(this, this)
|
||||
exe.ready = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected run(positionOrDelta: number) {
|
||||
let allfinished = true
|
||||
for (let i = 0, l = this.executors.length; i < l; i += 1) {
|
||||
const exe = this.executors[i]
|
||||
const converged = exe.run.call(this, this, positionOrDelta)
|
||||
exe.finished = exe.finished || converged === true
|
||||
allfinished = allfinished && exe.finished
|
||||
}
|
||||
|
||||
return allfinished
|
||||
}
|
||||
|
||||
protected remember(method: string, morpher: Morpher<any, any, any>) {
|
||||
// Save the morpher to the morpher list so that we can retarget it later
|
||||
this.history[method] = {
|
||||
morpher,
|
||||
executor: this.executors[this.executors.length - 1],
|
||||
}
|
||||
|
||||
// We have to resume the timeline in case a controller
|
||||
// is already done without beeing ever run
|
||||
// This can happen when e.g. this is done:
|
||||
// anim = el.animate(new SVG.Spring)
|
||||
// and later
|
||||
// anim.move(...)
|
||||
if (this.declarative) {
|
||||
if (this.timeline) {
|
||||
this.timeline.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected retarget<TTarget, TExtra>(
|
||||
method: string,
|
||||
target: TTarget,
|
||||
extra?: TExtra,
|
||||
) {
|
||||
if (this.history[method]) {
|
||||
const { morpher, executor } = this.history[method]
|
||||
|
||||
// If the previous executor wasn't even prepared, drop it.
|
||||
if (!executor.ready) {
|
||||
const index = this.executors.indexOf(executor)
|
||||
this.executors.splice(index, 1)
|
||||
return false
|
||||
}
|
||||
|
||||
if (executor.retarget) {
|
||||
// For the case of transformations, we use the special
|
||||
// retarget function which has access to the outer scope
|
||||
executor.retarget.call(this, this, target, extra)
|
||||
} else {
|
||||
morpher.to(target)
|
||||
}
|
||||
|
||||
executor.finished = false
|
||||
|
||||
if (this.timeline) {
|
||||
this.timeline.play()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Animator {
|
||||
export type Owner = Record<string, any>
|
||||
export type EventNames = 'start' | 'step' | 'finished'
|
||||
export type EventHandler<T> = (animator: T) => any
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import type { Morpher } from '../morpher/morpher'
|
||||
|
||||
export type PrepareMethod<TAnimator> = (
|
||||
this: TAnimator,
|
||||
animator: TAnimator,
|
||||
) => any
|
||||
|
||||
export type RunMethod<TAnimator> = (
|
||||
this: TAnimator,
|
||||
animator: TAnimator,
|
||||
positionOrDelta: number,
|
||||
) => any
|
||||
|
||||
export type RetargetMethod<TAnimator, TTarget = any, TExtra = any> = (
|
||||
this: TAnimator,
|
||||
animator: TAnimator,
|
||||
target: TTarget,
|
||||
extra: TExtra,
|
||||
) => any
|
||||
|
||||
interface Executor<TAnimator> {
|
||||
prepare: PrepareMethod<TAnimator>
|
||||
run: RunMethod<TAnimator>
|
||||
retarget?: RetargetMethod<TAnimator> | null
|
||||
ready: boolean
|
||||
finished: boolean
|
||||
isTransform?: boolean
|
||||
}
|
||||
|
||||
export type Executors<TAnimator> = Executor<TAnimator>[]
|
||||
|
||||
export interface History<TAnimator> {
|
||||
[method: string]: {
|
||||
morpher: Morpher<any, any, any>
|
||||
executor: Executor<TAnimator>
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import type { Options, When } from '../types'
|
||||
import { Stepper } from '../stepper/stepper'
|
||||
|
||||
export namespace Util {
|
||||
let id = 0
|
||||
export function generateId() {
|
||||
const ret = id
|
||||
id += 1
|
||||
return ret
|
||||
}
|
||||
|
||||
export const defaults = {
|
||||
duration: 400,
|
||||
delay: 0,
|
||||
}
|
||||
|
||||
export function sanitise(
|
||||
duration: number | Options = defaults.duration,
|
||||
delay: number = defaults.delay,
|
||||
when: When = 'after',
|
||||
) {
|
||||
let times = 1
|
||||
let swing = false
|
||||
let wait = 0
|
||||
|
||||
if (typeof duration === 'object' && !(duration instanceof Stepper)) {
|
||||
const options = duration
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
duration = duration.duration || defaults.duration
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
delay = options.delay || delay
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
when = options.when || when
|
||||
swing = options.swing || swing
|
||||
times = options.times || times
|
||||
wait = options.wait || wait
|
||||
}
|
||||
|
||||
return {
|
||||
delay,
|
||||
swing,
|
||||
times,
|
||||
wait,
|
||||
when,
|
||||
duration: duration as number,
|
||||
}
|
||||
}
|
||||
|
||||
// export function create<TType extends typeof Animator>(
|
||||
// Type: TType,
|
||||
// element: Primer,
|
||||
// duration?: Partial<Options> | number,
|
||||
// delay?: number,
|
||||
// when?: When,
|
||||
// ) {
|
||||
// const o = sanitise(duration, delay, when)
|
||||
// const timeline = element.timeline()
|
||||
// return new Type(o.duration)
|
||||
// .loop(o)
|
||||
// .element(element)
|
||||
// .timeline(timeline.play())
|
||||
// .schedule(o.delay, o.when)
|
||||
// }
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './animator/animator'
|
||||
export * from './scheduler/timeline'
|
@ -1,19 +0,0 @@
|
||||
import { Box } from '../../struct/box'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableBox
|
||||
extends Box
|
||||
implements Morphable<Box.BoxArray, Box.BoxArray>
|
||||
{
|
||||
fromArray(arr: Box.BoxArray) {
|
||||
this.x = arr[0]
|
||||
this.y = arr[0]
|
||||
this.width = arr[0]
|
||||
this.height = arr[0]
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Color } from '../../struct/color'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableColor
|
||||
extends Color
|
||||
implements Morphable<Color.RGBA, Color.RGBA>
|
||||
{
|
||||
fromArray(arr: Color.RGBA) {
|
||||
this.set(...arr)
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableFallback<T = any> implements Morphable<T[], T> {
|
||||
value: T
|
||||
|
||||
constructor(input: any) {
|
||||
this.value = Array.isArray(input) ? input[0] : input
|
||||
}
|
||||
|
||||
fromArray(arr: T[]) {
|
||||
this.value = arr[0]
|
||||
return this
|
||||
}
|
||||
|
||||
toArray(): T[] {
|
||||
return [this.value]
|
||||
}
|
||||
|
||||
toValue(): T {
|
||||
return this.value
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { Matrix } from '../../struct/matrix'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableMatrix
|
||||
extends Matrix
|
||||
implements Morphable<Matrix.MatrixArray, Matrix.MatrixArray>
|
||||
{
|
||||
fromArray(arr: Matrix.MatrixArray) {
|
||||
this.a = arr[0]
|
||||
this.b = arr[1]
|
||||
this.c = arr[2]
|
||||
this.d = arr[3]
|
||||
this.e = arr[4]
|
||||
this.f = arr[5]
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// eslint-disable-next-line
|
||||
export class Morphable<TArray extends any[], TValue> {
|
||||
constructor()
|
||||
constructor(arg: any)
|
||||
constructor(...args: any[])
|
||||
// eslint-disable-next-line
|
||||
constructor(...args: any[]) {}
|
||||
}
|
||||
|
||||
export interface Morphable<TArray extends any[], TValue> {
|
||||
fromArray(arr: TArray): this
|
||||
toArray(): TArray
|
||||
toValue(): TValue
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import { Easing } from '../stepper/easing'
|
||||
import { Stepper } from '../stepper/stepper'
|
||||
import { Util } from './util'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class Morpher<TArray extends any[], TInput, TValue> {
|
||||
protected Type: typeof Morphable | null = null
|
||||
protected stepper: Stepper
|
||||
protected source: TArray = [] as any
|
||||
protected target: TArray = [] as any
|
||||
protected contexts: Stepper.Context[]
|
||||
protected instance: Morphable<TArray, TValue>
|
||||
|
||||
constructor(stepper?: Stepper) {
|
||||
this.stepper = stepper || new Easing()
|
||||
}
|
||||
|
||||
ease(): Stepper
|
||||
ease(stepper: Stepper): this
|
||||
ease(stepper?: Stepper) {
|
||||
if (stepper == null) {
|
||||
return this.stepper
|
||||
}
|
||||
|
||||
this.stepper = stepper
|
||||
return this
|
||||
}
|
||||
|
||||
at(pos: number): TValue {
|
||||
const current = this.source.map((val, index) =>
|
||||
this.stepper.step(
|
||||
val,
|
||||
this.target[index],
|
||||
pos,
|
||||
this.contexts[index],
|
||||
this.contexts,
|
||||
),
|
||||
)
|
||||
|
||||
return this.instance.fromArray(current as TArray).toValue()
|
||||
}
|
||||
|
||||
done(): boolean {
|
||||
return this.contexts
|
||||
.map((context) => this.stepper.done(context))
|
||||
.reduce((memo, curr) => memo && curr, true)
|
||||
}
|
||||
|
||||
from(): TArray
|
||||
from(val: TInput): this
|
||||
from(val?: TInput) {
|
||||
if (val == null) {
|
||||
return this.source
|
||||
}
|
||||
|
||||
this.source = this.set(val)
|
||||
return this
|
||||
}
|
||||
|
||||
to(): TArray
|
||||
to(val: TInput): this
|
||||
to(val?: TInput) {
|
||||
if (val == null) {
|
||||
return this.target
|
||||
}
|
||||
|
||||
this.target = this.set(val)
|
||||
return this
|
||||
}
|
||||
|
||||
type(): typeof Morphable
|
||||
type(t: typeof Morphable): this
|
||||
type(t?: typeof Morphable) {
|
||||
if (t == null) {
|
||||
return this.Type
|
||||
}
|
||||
|
||||
this.Type = t
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
protected set(value: TInput): TArray {
|
||||
if (this.Type == null) {
|
||||
this.type(Util.getClassForType(value))
|
||||
}
|
||||
|
||||
const Ctor = this.type()
|
||||
const morphable = new Ctor<TArray, TValue>(value)
|
||||
|
||||
const arr = morphable.toArray()
|
||||
|
||||
if (this.instance == null) {
|
||||
this.instance = new Ctor()
|
||||
}
|
||||
|
||||
if (this.contexts == null) {
|
||||
this.contexts = arr.map(() => ({ done: true }))
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { NumberArray } from '../../struct/number-array'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableNumberArray
|
||||
extends NumberArray
|
||||
implements Morphable<number[], number[]>
|
||||
{
|
||||
fromArray(arr: number[]) {
|
||||
this.length = 0
|
||||
this.push(...arr)
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { Morphable } from './morphable'
|
||||
import { Util } from './util'
|
||||
|
||||
export class MorphableObject<
|
||||
T extends Record<string, any> = Record<string, any>,
|
||||
> implements Morphable<any[], T>
|
||||
{
|
||||
protected values: any[]
|
||||
|
||||
constructor(input?: any[] | T) {
|
||||
if (input != null) {
|
||||
if (Array.isArray(input)) {
|
||||
this.fromArray(input)
|
||||
} else if (typeof input === 'object') {
|
||||
const entries: [string, typeof Morphable, number, ...any[]][] = []
|
||||
Object.keys(input).forEach((key) => {
|
||||
const Type = Util.getClassForType(input[key])
|
||||
const val = new Type(input[key]).toArray()
|
||||
entries.push([key, Type, val.length, ...val])
|
||||
})
|
||||
|
||||
entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
||||
this.values = entries.reduce<any[]>(
|
||||
(memo, curr) => memo.concat(curr),
|
||||
[],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.values == null) {
|
||||
this.values = []
|
||||
}
|
||||
}
|
||||
|
||||
fromArray(arr: any[]) {
|
||||
this.values = arr.slice()
|
||||
return this
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return this.values.slice()
|
||||
}
|
||||
|
||||
toValue(): T {
|
||||
const obj: Record<string, any> = {}
|
||||
const arr = this.values
|
||||
while (arr.length) {
|
||||
const key = arr.shift()
|
||||
const Type = arr.shift()
|
||||
const len = arr.shift()
|
||||
const values = arr.splice(0, len)
|
||||
obj[key] = new Type().formArray(values).toValue()
|
||||
}
|
||||
|
||||
return obj as T
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import type { Path } from '../../vector/path/path'
|
||||
import { PathArray } from '../../struct/path-array'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphablePathArray
|
||||
extends PathArray
|
||||
implements Morphable<Path.Segment[], Path.Segment[]>
|
||||
{
|
||||
fromArray(arr: any[]) {
|
||||
this.length = 0
|
||||
this.push(...this.parse(arr))
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { PointArray } from '../../struct/point-array'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphablePointArray
|
||||
extends PointArray
|
||||
implements Morphable<[number, number][], [number, number][]>
|
||||
{
|
||||
fromArray(arr: any[]) {
|
||||
this.length = 0
|
||||
this.push(...this.parse(arr))
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableTransform
|
||||
implements Morphable<MorphableTransform.Array, MorphableTransform.Array>
|
||||
{
|
||||
scaleX: number
|
||||
scaleY: number
|
||||
shear: number
|
||||
rotate: number
|
||||
translateX: number
|
||||
translateY: number
|
||||
originX: number
|
||||
originY: number
|
||||
|
||||
fromArray(arr: MorphableTransform.Array) {
|
||||
const obj = {
|
||||
scaleX: arr[0],
|
||||
scaleY: arr[1],
|
||||
shear: arr[2],
|
||||
rotate: arr[3],
|
||||
translateX: arr[4],
|
||||
translateY: arr[5],
|
||||
originX: arr[6],
|
||||
originY: arr[7],
|
||||
}
|
||||
Object.assign(this, MorphableTransform.defaults, obj)
|
||||
return this
|
||||
}
|
||||
|
||||
toArray(): MorphableTransform.Array {
|
||||
return [
|
||||
this.scaleX,
|
||||
this.scaleY,
|
||||
this.shear,
|
||||
this.rotate,
|
||||
this.translateX,
|
||||
this.translateY,
|
||||
this.originX,
|
||||
this.originY,
|
||||
]
|
||||
}
|
||||
|
||||
toValue(): MorphableTransform.Array {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MorphableTransform {
|
||||
export const defaults = {
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
shear: 0,
|
||||
rotate: 0,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
}
|
||||
|
||||
export type Array = [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
]
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { UnitNumber } from '../../struct/unit-number'
|
||||
import { Morphable } from './morphable'
|
||||
|
||||
export class MorphableUnitNumber
|
||||
extends UnitNumber
|
||||
implements Morphable<UnitNumber.UnitNumberArray, UnitNumber.UnitNumberArray>
|
||||
{
|
||||
fromArray(arr: UnitNumber.UnitNumberArray) {
|
||||
this.unit = arr[1] || ''
|
||||
if (typeof arr[0] === 'string') {
|
||||
const obj = UnitNumber.parse(arr[0])
|
||||
if (obj) {
|
||||
this.value = obj.value
|
||||
this.unit = obj.unit
|
||||
}
|
||||
} else {
|
||||
this.value = arr[0]
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
toValue() {
|
||||
return this.toArray()
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
import { Morphable } from './morphable'
|
||||
import { MorphableBox } from './box'
|
||||
import { MorphableColor } from './color'
|
||||
import { MorphableMatrix } from './matrix'
|
||||
import { MorphableObject } from './object'
|
||||
import { MorphableFallback } from './fallback'
|
||||
import { MorphablePathArray } from './path-array'
|
||||
import { MorphablePointArray } from './point-array'
|
||||
import { MorphableTransform } from './transform'
|
||||
import { MorphableUnitNumber } from './unit-number'
|
||||
import { MorphableNumberArray } from './number-array'
|
||||
|
||||
export namespace Util {
|
||||
const delimiter = /[\s,]+/
|
||||
const isPathLetter = /[MLHVCSQTAZ]/i
|
||||
const presets = [
|
||||
MorphableBox,
|
||||
MorphableColor,
|
||||
MorphableMatrix,
|
||||
MorphableFallback,
|
||||
MorphableNumberArray,
|
||||
MorphableUnitNumber,
|
||||
MorphableObject,
|
||||
MorphablePathArray,
|
||||
MorphablePointArray,
|
||||
MorphableTransform,
|
||||
]
|
||||
|
||||
export function getClassForType(value: any): typeof Morphable {
|
||||
const type = typeof value
|
||||
|
||||
if (type === 'number') {
|
||||
return MorphableUnitNumber
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
if (MorphableColor.isColor(value)) {
|
||||
return MorphableColor
|
||||
}
|
||||
|
||||
if (delimiter.test(value)) {
|
||||
return isPathLetter.test(value)
|
||||
? MorphablePathArray
|
||||
: MorphableNumberArray
|
||||
}
|
||||
|
||||
if (MorphableUnitNumber.REGEX_NUMBER_UNIT.test(value)) {
|
||||
return MorphableUnitNumber
|
||||
}
|
||||
|
||||
return MorphableFallback as any
|
||||
}
|
||||
|
||||
if (presets.includes(value.constructor)) {
|
||||
return value.constructor as typeof Morphable
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return MorphableNumberArray
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
return MorphableObject
|
||||
}
|
||||
|
||||
return MorphableFallback as any
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import { Global } from '../../global/global'
|
||||
|
||||
export class MockRAF {
|
||||
protected realRAF: typeof requestAnimationFrame
|
||||
protected mockRAF: typeof requestAnimationFrame
|
||||
protected realCAF: typeof cancelAnimationFrame
|
||||
protected mockCAF: typeof cancelAnimationFrame
|
||||
protected realPerf: { now: () => number }
|
||||
protected mockPerf: { now: () => number }
|
||||
protected callbacks: FrameRequestCallback[] = []
|
||||
protected nextTime = 0
|
||||
|
||||
constructor() {
|
||||
this.mockRAF = (fn: FrameRequestCallback) => {
|
||||
this.callbacks.push(fn)
|
||||
return this.callbacks.length - 1
|
||||
}
|
||||
|
||||
this.mockCAF = (requestID: number) => {
|
||||
this.callbacks.splice(requestID, 1)
|
||||
}
|
||||
|
||||
this.mockPerf = {
|
||||
now: () => this.nextTime,
|
||||
}
|
||||
}
|
||||
|
||||
install(win: Global.WindowType) {
|
||||
this.realRAF = win.requestAnimationFrame
|
||||
this.realCAF = win.cancelAnimationFrame
|
||||
this.realPerf = win.performance
|
||||
win.requestAnimationFrame = this.mockRAF
|
||||
win.cancelAnimationFrame = this.mockCAF
|
||||
const w = win as any
|
||||
w.performance = this.mockPerf
|
||||
}
|
||||
|
||||
uninstall(win: Global.WindowType) {
|
||||
const w = win as any
|
||||
|
||||
win.requestAnimationFrame = this.realRAF
|
||||
win.cancelAnimationFrame = this.realCAF
|
||||
w.performance = this.realPerf
|
||||
|
||||
this.nextTime = 0
|
||||
this.callbacks = []
|
||||
}
|
||||
|
||||
tick(dt = 1) {
|
||||
this.nextTime += dt
|
||||
const callbacks = this.callbacks
|
||||
this.callbacks = []
|
||||
callbacks.forEach((fn) => fn(this.nextTime))
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
import { Queue } from './queue'
|
||||
|
||||
describe('Queue', () => {
|
||||
describe('first()', () => {
|
||||
it('should return null if no item in the queue', () => {
|
||||
const queue = new Queue()
|
||||
expect(queue.first()).toEqual(null)
|
||||
})
|
||||
|
||||
it('should return the first value in the queue', () => {
|
||||
const queue = new Queue<number>()
|
||||
queue.push(1)
|
||||
expect(queue.first()).toBe(1)
|
||||
queue.push(2)
|
||||
expect(queue.first()).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('last()', () => {
|
||||
it('should return null if no item in the queue', () => {
|
||||
const queue = new Queue()
|
||||
expect(queue.last()).toEqual(null)
|
||||
})
|
||||
|
||||
it('should return the last value added', () => {
|
||||
const queue = new Queue()
|
||||
queue.push(1)
|
||||
expect(queue.last()).toBe(1)
|
||||
queue.push(2)
|
||||
expect(queue.last()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('push()', () => {
|
||||
it('should add an element to the end of the queue', () => {
|
||||
const queue = new Queue()
|
||||
queue.push(1)
|
||||
queue.push(2)
|
||||
queue.push(3)
|
||||
|
||||
expect(queue.first()).toBe(1)
|
||||
expect(queue.last()).toBe(3)
|
||||
})
|
||||
|
||||
it('should add an item to the end of the queue', () => {
|
||||
const queue = new Queue()
|
||||
queue.push(1)
|
||||
const item = queue.push(2)
|
||||
queue.push(3)
|
||||
queue.remove(item)
|
||||
queue.push(item)
|
||||
|
||||
expect(queue.first()).toBe(1)
|
||||
expect(queue.last()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove()', () => {
|
||||
it('should remove the given item from the queue', () => {
|
||||
const queue = new Queue()
|
||||
queue.push(1)
|
||||
queue.push(2)
|
||||
const item = queue.push(3)
|
||||
|
||||
queue.remove(item)
|
||||
|
||||
expect(queue.last()).toBe(2)
|
||||
expect(queue.first()).toBe(1)
|
||||
})
|
||||
|
||||
it('should remove the given item from the queue', () => {
|
||||
const queue = new Queue()
|
||||
const item = queue.push(1)
|
||||
queue.push(2)
|
||||
queue.push(3)
|
||||
|
||||
queue.remove(item)
|
||||
|
||||
expect(queue.last()).toBe(3)
|
||||
expect(queue.first()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shift()', () => {
|
||||
it('should return nothing if queue is empty', () => {
|
||||
const queue = new Queue()
|
||||
const val = queue.shift()
|
||||
expect(val).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should return the first item of the queue and should remove it', () => {
|
||||
const queue = new Queue()
|
||||
queue.push(1)
|
||||
queue.push(2)
|
||||
queue.push(3)
|
||||
|
||||
const val = queue.shift()
|
||||
|
||||
expect(queue.last()).toBe(3)
|
||||
expect(queue.first()).toBe(2)
|
||||
|
||||
expect(val).toBe(1)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,91 +0,0 @@
|
||||
export class Queue<T> {
|
||||
protected firstItem: Queue.Item<T> | null
|
||||
protected lastItem: Queue.Item<T> | null
|
||||
|
||||
constructor() {
|
||||
this.firstItem = null
|
||||
this.lastItem = null
|
||||
}
|
||||
|
||||
first() {
|
||||
return this.firstItem ? this.firstItem.value : null
|
||||
}
|
||||
|
||||
last() {
|
||||
return this.lastItem ? this.lastItem.value : null
|
||||
}
|
||||
|
||||
shift() {
|
||||
const remove = this.firstItem
|
||||
if (!remove) {
|
||||
return null
|
||||
}
|
||||
|
||||
this.firstItem = remove.next
|
||||
if (this.firstItem) {
|
||||
this.firstItem.prev = null
|
||||
}
|
||||
this.lastItem = this.firstItem ? this.lastItem : null
|
||||
return remove.value
|
||||
}
|
||||
|
||||
push(value: T | Queue.Item<T>) {
|
||||
const o = value as any
|
||||
const item: Queue.Item<T> =
|
||||
typeof o === 'object' &&
|
||||
typeof o.value !== 'undefined' &&
|
||||
typeof o.prev !== 'undefined' &&
|
||||
typeof o.next !== 'undefined'
|
||||
? o
|
||||
: { value: o, next: null, prev: null }
|
||||
|
||||
if (this.lastItem) {
|
||||
item.prev = this.lastItem
|
||||
this.lastItem.next = item
|
||||
this.lastItem = item
|
||||
} else {
|
||||
this.lastItem = item
|
||||
this.firstItem = item
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
remove(item: Queue.Item<T>) {
|
||||
if (item.prev) {
|
||||
item.prev.next = item.next
|
||||
}
|
||||
|
||||
if (item.next) {
|
||||
item.next.prev = item.prev
|
||||
}
|
||||
|
||||
if (item === this.lastItem) {
|
||||
this.lastItem = item.prev
|
||||
}
|
||||
|
||||
if (item === this.firstItem) {
|
||||
this.firstItem = item.next
|
||||
}
|
||||
|
||||
item.prev = null
|
||||
item.next = null
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Queue {
|
||||
export interface Item<T> {
|
||||
value: T
|
||||
prev: Item<T> | null
|
||||
next: Item<T> | null
|
||||
}
|
||||
|
||||
export function isItem<T>(o: any): o is Item<T> {
|
||||
return (
|
||||
typeof o === 'object' &&
|
||||
typeof o.value !== 'undefined' &&
|
||||
typeof o.prev !== 'undefined' &&
|
||||
typeof o.next !== 'undefined'
|
||||
)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { Timeline } from './timeline'
|
||||
import { Primer } from '../../dom/primer'
|
||||
import { applyMixins } from '../../util/mixin'
|
||||
|
||||
export class ElementExtension {
|
||||
protected timeline: Timeline
|
||||
|
||||
scheduler(): Timeline
|
||||
scheduler(timeline: Timeline): this
|
||||
scheduler(timeline?: Timeline) {
|
||||
if (timeline == null) {
|
||||
if (this.timeline == null) {
|
||||
this.timeline = new Timeline()
|
||||
}
|
||||
return this.timeline
|
||||
}
|
||||
|
||||
this.timeline = timeline
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
declare module '../../dom/primer/primer' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Primer<TElement extends Element = Element>
|
||||
extends ElementExtension {}
|
||||
}
|
||||
|
||||
applyMixins(Primer, ElementExtension)
|
@ -1,393 +0,0 @@
|
||||
import type { When, Now } from '../types'
|
||||
import type { Animator } from '../animator/animator'
|
||||
import { Queue } from './queue'
|
||||
import { Timing } from './timing'
|
||||
|
||||
export class Timeline {
|
||||
public readonly step: () => this
|
||||
public readonly stepImmediately: () => this
|
||||
public readonly now: Now
|
||||
|
||||
protected paused = true
|
||||
|
||||
/**
|
||||
* The speed of the timeline.
|
||||
*/
|
||||
protected v = 1
|
||||
/**
|
||||
* The current time of the timeline.
|
||||
*/
|
||||
protected t = 0
|
||||
/**
|
||||
* The previous step time of the timeline.
|
||||
*/
|
||||
protected o = 0
|
||||
|
||||
protected persisted: number | boolean = 0
|
||||
|
||||
protected timestamp = 0
|
||||
|
||||
protected animators: {
|
||||
animator: Timeline.AnyAnimator
|
||||
start: number
|
||||
persist: number | boolean
|
||||
}[] = []
|
||||
protected animatorIds: number[] = []
|
||||
protected previousAnimatorId = -1
|
||||
|
||||
protected nextFrame: Queue.Item<Timing.Frame> | null = null
|
||||
|
||||
constructor()
|
||||
constructor(now?: Now)
|
||||
constructor(now: Now = Timing.timer().now) {
|
||||
this.now = now
|
||||
this.step = () => this.stepImpl(false)
|
||||
this.stepImmediately = () => this.stepImpl(true)
|
||||
}
|
||||
|
||||
isActive() {
|
||||
return this.nextFrame != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the speed of the timeline.
|
||||
*/
|
||||
speed(): number
|
||||
/**
|
||||
* Set the speed of the timeline. Negative speeds will reverse the timeline.
|
||||
*/
|
||||
speed(v: number): this
|
||||
speed(v?: number) {
|
||||
if (v == null) {
|
||||
return this.v
|
||||
}
|
||||
|
||||
this.v = v
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the timeline in reverse basically going back in time.
|
||||
*/
|
||||
reverse(): this
|
||||
reverse(yes: boolean): this
|
||||
reverse(yes?: boolean) {
|
||||
const speed = this.speed()
|
||||
if (yes == null) {
|
||||
return this.speed(-speed)
|
||||
}
|
||||
|
||||
const positive = Math.abs(speed)
|
||||
return this.speed(yes ? -positive : positive)
|
||||
}
|
||||
|
||||
persist(): number | boolean
|
||||
persist(dt: number): this
|
||||
persist(forever: boolean): this
|
||||
persist(dtOrForever?: number | boolean) {
|
||||
if (dtOrForever == null) {
|
||||
return this.persisted
|
||||
}
|
||||
|
||||
this.persisted = dtOrForever
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current time of the timeline.
|
||||
*/
|
||||
time(): number
|
||||
/**
|
||||
* Set the current time of the timeline.
|
||||
*/
|
||||
time(t: number): this
|
||||
time(t?: number) {
|
||||
if (t == null) {
|
||||
return this.t
|
||||
}
|
||||
this.t = t
|
||||
return this.peek(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset the time by a delta.
|
||||
*/
|
||||
offset(delta: number) {
|
||||
return this.time(this.t + delta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finishes the whole timeline. All values are set to their corresponding
|
||||
* end values and every animation gets fullfilled.
|
||||
*/
|
||||
finish() {
|
||||
const ends = this.animators.map(
|
||||
({ start, animator }) => start + animator.quantity(),
|
||||
)
|
||||
const terminal = Math.max(0, ...ends)
|
||||
this.time(terminal + 1)
|
||||
return this.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timeline and sets the time back to zero.
|
||||
*/
|
||||
stop() {
|
||||
this.time(0)
|
||||
return this.pause()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the timeline.
|
||||
*/
|
||||
pause() {
|
||||
this.paused = true
|
||||
return this.peek()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpauses the timeline and continue the animation.
|
||||
*/
|
||||
play() {
|
||||
this.paused = false
|
||||
return this.update().peek()
|
||||
}
|
||||
|
||||
protected update() {
|
||||
if (!this.isActive()) {
|
||||
// Makes sure, that after pausing the time doesn't jump
|
||||
this.updateTimestamp()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
protected updateTimestamp() {
|
||||
const now = this.now()
|
||||
const delta = now - this.timestamp
|
||||
this.timestamp = now
|
||||
return delta
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we are running and continues the animation.
|
||||
*/
|
||||
peek(immediately = false) {
|
||||
Timing.cancelFrame(this.nextFrame)
|
||||
this.nextFrame = null
|
||||
|
||||
if (immediately) {
|
||||
return this.stepImmediately()
|
||||
}
|
||||
|
||||
if (this.paused) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.nextFrame = Timing.frame(this.step)
|
||||
return this
|
||||
}
|
||||
|
||||
schedule<TAnimator extends Timeline.AnyAnimator>(): {
|
||||
start: number
|
||||
end: number
|
||||
duration: number
|
||||
animator: TAnimator
|
||||
}[]
|
||||
/**
|
||||
* Schedules a runner on the timeline.
|
||||
*/
|
||||
schedule<TAnimator extends Timeline.AnyAnimator>(
|
||||
animator: TAnimator,
|
||||
delay: number,
|
||||
when?: When,
|
||||
): this
|
||||
schedule<TAnimator extends Timeline.AnyAnimator>(
|
||||
animator?: TAnimator,
|
||||
delay = 0,
|
||||
when?: When,
|
||||
) {
|
||||
if (animator == null) {
|
||||
return this.animators.map(({ start, animator }) => {
|
||||
const duration = animator.quantity()
|
||||
const end = start + duration
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
animator,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The start time for the next animation can either be given explicitly,
|
||||
// derived from the current timeline time or it can be relative to the
|
||||
// last start time to chain animations direclty
|
||||
|
||||
let start = 0
|
||||
|
||||
if (when == null || when === 'after') {
|
||||
// Plays the animation after the animation which comes last on the
|
||||
// timeline. If there is none, the animation is played right now.
|
||||
// Take the last time and increment
|
||||
start = this.getPreviousEndTime() + delay
|
||||
} else if (when === 'start') {
|
||||
// Schedules the animation to run to an absolute time on your timeline.
|
||||
start = delay
|
||||
} else if (when === 'now') {
|
||||
// Plays the animation right now.
|
||||
start = this.t + delay
|
||||
} else if (when === 'relative') {
|
||||
// Schedules the animation to play relative to its old start time.
|
||||
const info = this.getAnimator(animator.id)
|
||||
if (info) {
|
||||
start = info.start + delay
|
||||
}
|
||||
} else if (when === 'with') {
|
||||
const info = this.getPreviousAnimator()
|
||||
const lastStartTime = info ? info.start : this.t
|
||||
start = lastStartTime + delay
|
||||
} else {
|
||||
throw new Error('Invalid value for the "when" parameter')
|
||||
}
|
||||
|
||||
animator.unschedule()
|
||||
animator.scheduler(this)
|
||||
|
||||
const persist = animator.persist()
|
||||
const meta = {
|
||||
start,
|
||||
animator,
|
||||
persist: persist == null ? this.persist() : persist,
|
||||
}
|
||||
|
||||
this.previousAnimatorId = animator.id
|
||||
this.animators.push(meta)
|
||||
this.animators.sort((a, b) => a.start - b.start)
|
||||
this.animatorIds = this.animators.map((i) => i.animator.id)
|
||||
|
||||
this.update().peek()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the animator from this timeline.
|
||||
*/
|
||||
unschedule<TAnimator extends Timeline.AnyAnimator>(animator: TAnimator) {
|
||||
const index = this.animatorIds.indexOf(animator.id)
|
||||
if (index < 0) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.animators.splice(index, 1)
|
||||
this.animatorIds.splice(index, 1)
|
||||
|
||||
animator.scheduler(null)
|
||||
return this
|
||||
}
|
||||
|
||||
protected stepImpl(immediately = false) {
|
||||
// Get the time delta from the last time and update the time
|
||||
let delta = this.updateTimestamp()
|
||||
|
||||
if (immediately) {
|
||||
delta = 0
|
||||
}
|
||||
|
||||
const dtTime = this.v * delta + (this.t - this.o)
|
||||
|
||||
if (!immediately) {
|
||||
this.t += dtTime
|
||||
this.t = this.t < 0 ? 0 : this.t
|
||||
}
|
||||
this.o = this.t
|
||||
// this.fire('time', this.currentTime)
|
||||
|
||||
// However, reseting in insertion order leads to bugs. Considering the case,
|
||||
// where 2 animators change the same attriute but in different times,
|
||||
// reseting both of them will lead to the case where the later defined
|
||||
// animator always wins the reset even if the other animator started earlier
|
||||
// and therefore should win the attribute battle
|
||||
// this can be solved by reseting them backwards
|
||||
for (let i = this.animators.length - 1; i >= 0; i -= 1) {
|
||||
const { start, animator } = this.animators[i]
|
||||
const delta = this.t - start
|
||||
// Dont run animator if not started yet and try to reset it
|
||||
if (delta <= 0) {
|
||||
animator.reset()
|
||||
}
|
||||
}
|
||||
|
||||
let next = false
|
||||
for (let i = 0, l = this.animators.length; i < l; i += 1) {
|
||||
// Get and run the current animator and ignore it if its inactive
|
||||
const { animator, start, persist } = this.animators[i]
|
||||
if (!animator.active()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const delta = this.t - start
|
||||
if (delta <= 0) {
|
||||
// Dont run animator if not started yet
|
||||
next = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Adjust dt to make sure that animation is on point
|
||||
const dt = delta < dtTime ? delta : dtTime
|
||||
animator.step(dt)
|
||||
|
||||
if (!animator.done) {
|
||||
next = true
|
||||
} else if (persist !== true) {
|
||||
const endTime = animator.quantity() - animator.time() + this.t
|
||||
const hold = persist === false ? 0 : persist
|
||||
|
||||
if (endTime + hold < this.t) {
|
||||
// Delete animator and correct index
|
||||
animator.unschedule()
|
||||
i -= 1
|
||||
l -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(next && !(this.v < 0 && this.t === 0)) ||
|
||||
(this.animatorIds.length > 0 && this.v < 0 && this.t > 0)
|
||||
) {
|
||||
this.peek()
|
||||
} else {
|
||||
this.pause()
|
||||
// this.fire('finished')
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
has(animatorId: number): boolean
|
||||
has<TAnimator extends Timeline.AnyAnimator>(animator: TAnimator): boolean
|
||||
has<TAnimator extends Timeline.AnyAnimator>(animatorId: number | TAnimator) {
|
||||
return this.animatorIds.includes(
|
||||
typeof animatorId === 'number' ? animatorId : animatorId.id,
|
||||
)
|
||||
}
|
||||
|
||||
protected getAnimator(animatorId: number) {
|
||||
return this.animators[this.animatorIds.indexOf(animatorId)] || null
|
||||
}
|
||||
|
||||
protected getPreviousAnimator() {
|
||||
return this.getAnimator(this.previousAnimatorId)
|
||||
}
|
||||
|
||||
protected getPreviousEndTime() {
|
||||
const meta = this.getPreviousAnimator()
|
||||
const duration = meta ? meta.animator.quantity() : 0
|
||||
const start = meta ? meta.start : this.t
|
||||
return start + duration
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Timeline {
|
||||
export type AnyAnimator = Animator<any, any>
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import sinon from 'sinon'
|
||||
import { Timing } from './timing'
|
||||
import { MockRAF } from './mock-raf'
|
||||
import { Global } from '../../global'
|
||||
|
||||
describe('Timing', () => {
|
||||
let raf: MockRAF
|
||||
|
||||
beforeEach(() => {
|
||||
raf = new MockRAF()
|
||||
raf.install(Global.getWindow())
|
||||
Timing.clean()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
raf.uninstall(Global.getWindow())
|
||||
})
|
||||
|
||||
describe('timeout()', () => {
|
||||
it('should call a function after a specific time', () => {
|
||||
const spy = sinon.spy()
|
||||
Timing.timeout(spy, 100)
|
||||
raf.tick(99)
|
||||
expect(spy.callCount).toBe(0)
|
||||
raf.tick()
|
||||
expect(spy.callCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelTimeout()', () => {
|
||||
it('should cancel a timeout which was created with timeout()', () => {
|
||||
const spy = sinon.spy()
|
||||
const id = Timing.timeout(spy, 100)
|
||||
Timing.clearTimeout(id)
|
||||
|
||||
expect(spy.called).toBeFalse()
|
||||
raf.tick(100)
|
||||
expect(spy.called).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
describe('frame()', () => {
|
||||
it('should call a function at the next animationFrame', () => {
|
||||
const spy = sinon.spy()
|
||||
Timing.frame(spy)
|
||||
expect(spy.called).toBeFalse()
|
||||
raf.tick()
|
||||
expect(spy.called).toBeTrue()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelFrame()', () => {
|
||||
it('should cancel a single frame which was created with frame()', () => {
|
||||
const spy = sinon.spy()
|
||||
const id = Timing.frame(spy)
|
||||
Timing.cancelFrame(id)
|
||||
expect(spy.called).toBeFalse()
|
||||
raf.tick()
|
||||
expect(spy.called).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
describe('immediate()', () => {
|
||||
it('should call a function at the next animationFrame but after all frames are processed', () => {
|
||||
const spy = sinon.spy()
|
||||
Timing.immediate(spy)
|
||||
expect(spy.called).toBeFalse()
|
||||
raf.tick()
|
||||
expect(spy.called).toBeTrue()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelImmediate()', () => {
|
||||
it('should cancel an immediate cakk which was created with immediate()', () => {
|
||||
const spy = sinon.spy()
|
||||
const id = Timing.immediate(spy)
|
||||
Timing.cancelImmediate(id)
|
||||
expect(spy.called).toBeFalse()
|
||||
raf.tick()
|
||||
expect(spy.called).toBeFalse()
|
||||
})
|
||||
})
|
||||
})
|
@ -1,116 +0,0 @@
|
||||
import { Global } from '../../global'
|
||||
import { Queue } from './queue'
|
||||
|
||||
export namespace Timing {
|
||||
export const timer = () => Global.window.performance || Global.window.Date
|
||||
|
||||
let frames = new Queue<Frame>()
|
||||
let timeouts = new Queue<Timeout>()
|
||||
let immediates = new Queue<Frame>()
|
||||
let nextTickId: number | null = null
|
||||
|
||||
export function clean() {
|
||||
frames = new Queue<Frame>()
|
||||
timeouts = new Queue<Timeout>()
|
||||
immediates = new Queue<Frame>()
|
||||
}
|
||||
|
||||
export function frame(fn: Callback) {
|
||||
const node = frames.push({ fn })
|
||||
tick()
|
||||
return node
|
||||
}
|
||||
|
||||
export function timeout(fn: Callback, delay = 0) {
|
||||
const node = timeouts.push({ fn, time: timer().now() + delay })
|
||||
tick()
|
||||
return node
|
||||
}
|
||||
|
||||
export function immediate(fn: Callback) {
|
||||
const node = immediates.push({ fn })
|
||||
tick()
|
||||
return node
|
||||
}
|
||||
|
||||
export function cancelFrame(node: Queue.Item<Frame> | null) {
|
||||
if (node) {
|
||||
frames.remove(node)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearTimeout(node: Queue.Item<Timeout> | null) {
|
||||
if (node) {
|
||||
timeouts.remove(node)
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelImmediate(node: Queue.Item<Frame> | null) {
|
||||
if (node) {
|
||||
immediates.remove(node)
|
||||
}
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (nextTickId === null) {
|
||||
nextTickId = requestAnimationFrame()
|
||||
}
|
||||
}
|
||||
|
||||
function requestAnimationFrame() {
|
||||
return Global.window.requestAnimationFrame(run)
|
||||
}
|
||||
|
||||
function run(now: number) {
|
||||
// Run all the timeouts we can run, if they are not ready yet, add them
|
||||
// to the end of the queue immediately!
|
||||
let nextTimeout = null
|
||||
const lastTimeout = timeouts.last()
|
||||
while ((nextTimeout = timeouts.shift())) {
|
||||
// Run the timeout if its time, or push it to the end
|
||||
if (now >= nextTimeout.time) {
|
||||
nextTimeout.fn(now)
|
||||
} else {
|
||||
timeouts.push(nextTimeout)
|
||||
}
|
||||
|
||||
// If we hit the last item, we should stop shifting out more items
|
||||
if (nextTimeout === lastTimeout) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Run all of the animation frames
|
||||
let nextFrame = null
|
||||
const lastFrame = frames.last()
|
||||
while ((nextFrame = frames.shift())) {
|
||||
nextFrame.fn(now)
|
||||
// If we hit the last item, we should stop shifting out more items
|
||||
if (nextFrame === lastFrame) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let nextImmediate = null
|
||||
while ((nextImmediate = immediates.shift())) {
|
||||
nextImmediate.fn(now)
|
||||
}
|
||||
|
||||
// If we have remaining timeouts or frames, run until we don't anymore
|
||||
const next = timeouts.first() || frames.first()
|
||||
nextTickId = next ? requestAnimationFrame() : null
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Timing {
|
||||
export type Callback = (now: number) => any
|
||||
|
||||
export interface Frame {
|
||||
fn: Callback
|
||||
}
|
||||
|
||||
export interface Timeout {
|
||||
fn: Callback
|
||||
time: number
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
import { Stepper } from './stepper'
|
||||
|
||||
export class PIDController extends Stepper {
|
||||
private p: number
|
||||
private i: number
|
||||
private d: number
|
||||
private windup: number | false
|
||||
|
||||
constructor(p = 0.1, i = 0.01, d = 0, windup = 1000) {
|
||||
super()
|
||||
this.p = p
|
||||
this.i = i
|
||||
this.d = d
|
||||
this.windup = windup
|
||||
}
|
||||
|
||||
step(
|
||||
from: string,
|
||||
to: string,
|
||||
delta: number,
|
||||
context: PIDController.Context,
|
||||
): string
|
||||
step(
|
||||
from: number,
|
||||
to: number,
|
||||
delta: number,
|
||||
context: PIDController.Context,
|
||||
): number
|
||||
step(
|
||||
from: string | number,
|
||||
to: string | number,
|
||||
delta: number,
|
||||
context: PIDController.Context,
|
||||
) {
|
||||
if (typeof from === 'string') {
|
||||
return from
|
||||
}
|
||||
|
||||
context.done = delta === Infinity
|
||||
|
||||
const origin = from as number
|
||||
const target = to as number
|
||||
|
||||
if (delta === Infinity) {
|
||||
return target
|
||||
}
|
||||
|
||||
if (delta === 0) {
|
||||
return origin
|
||||
}
|
||||
|
||||
const p = target - origin
|
||||
let i = (context.integral || 0) + p * delta
|
||||
const d = (p - (context.error || 0)) / delta
|
||||
const windup = this.windup
|
||||
|
||||
// antiwindup
|
||||
if (windup !== false) {
|
||||
i = Math.max(-windup, Math.min(i, windup))
|
||||
}
|
||||
|
||||
context.error = p
|
||||
context.integral = i
|
||||
context.done = Math.abs(p) < 0.001
|
||||
|
||||
return context.done
|
||||
? target
|
||||
: origin + (this.p * p + this.i * i + this.d * d)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace PIDController {
|
||||
export interface Context extends Stepper.Context {
|
||||
error: number
|
||||
integral: number
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import { Stepper } from './stepper'
|
||||
|
||||
export class SpringController extends Stepper {
|
||||
protected d: number
|
||||
protected k: number
|
||||
protected readonly duration: number
|
||||
protected readonly overshoot: number
|
||||
|
||||
constructor(duration = 500, overshoot = 0) {
|
||||
super()
|
||||
this.duration = duration
|
||||
this.overshoot = overshoot
|
||||
}
|
||||
|
||||
step(
|
||||
from: string,
|
||||
to: string,
|
||||
delta: number,
|
||||
context: SpringController.Context,
|
||||
): string
|
||||
step(
|
||||
from: number,
|
||||
to: number,
|
||||
delta: number,
|
||||
context: SpringController.Context,
|
||||
): number
|
||||
step(
|
||||
from: string | number,
|
||||
to: string | number,
|
||||
delta: number,
|
||||
context: SpringController.Context,
|
||||
) {
|
||||
if (typeof from === 'string') {
|
||||
return from
|
||||
}
|
||||
|
||||
const origin = from as number
|
||||
const target = to as number
|
||||
|
||||
context.done = delta === Infinity
|
||||
|
||||
if (delta === Infinity) {
|
||||
return target
|
||||
}
|
||||
|
||||
if (delta === 0) {
|
||||
return origin
|
||||
}
|
||||
|
||||
if (delta > 100) {
|
||||
delta = 16 // eslint-disable-line
|
||||
}
|
||||
|
||||
delta /= 1000 // eslint-disable-line
|
||||
|
||||
// Get the previous velocity
|
||||
const velocity = context.velocity || 0
|
||||
|
||||
// Apply the control to get the new position and store it
|
||||
const acceleration = -this.d * velocity - this.k * (origin - target)
|
||||
const newPosition =
|
||||
origin + velocity * delta + (acceleration * delta * delta) / 2
|
||||
|
||||
// Store the velocity
|
||||
context.velocity = velocity + acceleration * delta
|
||||
|
||||
// Figure out if we have converged, and if so, pass the value
|
||||
context.done = Math.abs(target - newPosition) + Math.abs(velocity) < 0.002
|
||||
return context.done ? target : newPosition
|
||||
}
|
||||
|
||||
protected recalculate() {
|
||||
// Apply the default parameters
|
||||
const duration = (this.duration || 500) / 1000
|
||||
const overshoot = this.overshoot || 0
|
||||
|
||||
// Calculate the PID natural response
|
||||
const eps = 1e-10
|
||||
const pi = Math.PI
|
||||
const os = Math.log(overshoot / 100 + eps)
|
||||
const zeta = -os / Math.sqrt(pi * pi + os * os)
|
||||
const wn = 3.9 / (zeta * duration)
|
||||
|
||||
// Calculate the Spring values
|
||||
this.d = 2 * zeta * wn
|
||||
this.k = wn * wn
|
||||
}
|
||||
}
|
||||
|
||||
export namespace SpringController {
|
||||
export interface Context extends Stepper.Context {
|
||||
velocity: number
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
import { Stepper } from './stepper'
|
||||
|
||||
export class Controller extends Stepper {
|
||||
stepper: typeof Stepper.prototype.step
|
||||
|
||||
constructor(fn: typeof Stepper.prototype.step) {
|
||||
super()
|
||||
this.stepper = fn
|
||||
}
|
||||
|
||||
step(
|
||||
from: string,
|
||||
to: string,
|
||||
delta: number,
|
||||
context: Stepper.Context,
|
||||
contexts: Stepper.Context[],
|
||||
): string
|
||||
step(
|
||||
from: number,
|
||||
to: number,
|
||||
delta: number,
|
||||
context: Stepper.Context,
|
||||
contexts: Stepper.Context[],
|
||||
): number
|
||||
step(
|
||||
from: string | number,
|
||||
to: string | number,
|
||||
delta: number,
|
||||
context: Stepper.Context,
|
||||
contexts: Stepper.Context[],
|
||||
) {
|
||||
return this.stepper(from, to, delta, context, contexts)
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export type Definition = (t: number) => number
|
||||
export type Names = keyof typeof presets
|
||||
|
||||
export const presets = {
|
||||
linear: (t: number) => t,
|
||||
easeOut: (t: number) => Math.sin((t * Math.PI) / 2),
|
||||
easeIn: (t: number) => -Math.cos((t * Math.PI) / 2) + 1,
|
||||
easeInOut: (t: number) => -Math.cos(t * Math.PI) / 2 + 0.5,
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import { Stepper } from './stepper'
|
||||
|
||||
export class Easing extends Stepper {
|
||||
protected readonly ease: Easing.Definition
|
||||
|
||||
constructor()
|
||||
constructor(ease: Easing.Names)
|
||||
constructor(ease: Easing.Definition)
|
||||
constructor(ease?: Easing.Names | Easing.Definition)
|
||||
constructor(ease: Easing.Names | Easing.Definition = 'linear') {
|
||||
super()
|
||||
if (typeof ease === 'string') {
|
||||
this.ease = Easing.presets[ease] || Easing.presets.linear
|
||||
} else {
|
||||
this.ease = ease
|
||||
}
|
||||
}
|
||||
|
||||
step(from: number, to: number, pos: number): number
|
||||
step(from: string, to: string, pos: number): string
|
||||
step(from: string | number, to: string | number, pos: number) {
|
||||
if (typeof from !== 'number') {
|
||||
return pos < 1 ? from : to
|
||||
}
|
||||
return +from + (+to - +from) * this.ease(pos)
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Easing {
|
||||
export type Definition = (t: number) => number
|
||||
export type Names = keyof typeof presets
|
||||
|
||||
export const presets = {
|
||||
linear: (pos: number) => pos,
|
||||
easeOut: (pos: number) => Math.sin((pos * Math.PI) / 2),
|
||||
easeIn: (pos: number) => -Math.cos((pos * Math.PI) / 2) + 1,
|
||||
easeInOut: (pos: number) => -Math.cos(pos * Math.PI) / 2 + 0.5,
|
||||
}
|
||||
|
||||
export const factories = {
|
||||
// see https://www.w3.org/TR/css-easing-1/#cubic-bezier-algo
|
||||
bezier(x1: number, y1: number, x2: number, y2: number) {
|
||||
return (t: number) => {
|
||||
if (t < 0) {
|
||||
if (x1 > 0) {
|
||||
return (y1 / x1) * t
|
||||
}
|
||||
|
||||
if (x2 > 0) {
|
||||
return (y2 / x2) * t
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
if (t > 1) {
|
||||
if (x2 < 1) {
|
||||
return ((1 - y2) / (1 - x2)) * t + (y2 - x2) / (1 - x2)
|
||||
}
|
||||
if (x1 < 1) {
|
||||
return ((1 - y1) / (1 - x1)) * t + (y1 - x1) / (1 - x1)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
return 3 * t * (1 - t) ** 2 * y1 + 3 * t ** 2 * (1 - t) * y2 + t ** 3
|
||||
}
|
||||
},
|
||||
// see https://www.w3.org/TR/css-easing-1/#step-timing-function-algo
|
||||
steps(
|
||||
steps: number,
|
||||
stepPosition:
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'both'
|
||||
| 'none'
|
||||
| 'jump-start'
|
||||
| 'jump-end' = 'end',
|
||||
) {
|
||||
// deal with "jump-" prefix
|
||||
const position = stepPosition.split('-').reverse()[0]
|
||||
|
||||
let jumps = steps
|
||||
if (position === 'none') {
|
||||
jumps -= 1
|
||||
} else if (position === 'both') {
|
||||
jumps += 1
|
||||
}
|
||||
|
||||
// The beforeFlag is essentially useless
|
||||
return (t: number, beforeFlag = false) => {
|
||||
// Step is called currentStep in referenced url
|
||||
let step = Math.floor(t * steps)
|
||||
const jumping = (t * step) % 1 === 0
|
||||
|
||||
if (position === 'start' || position === 'both') {
|
||||
step += 1
|
||||
}
|
||||
|
||||
if (beforeFlag && jumping) {
|
||||
step -= 1
|
||||
}
|
||||
|
||||
if (t >= 0 && step < 0) {
|
||||
step = 0
|
||||
}
|
||||
|
||||
if (t <= 1 && step > jumps) {
|
||||
step = jumps
|
||||
}
|
||||
|
||||
return step / jumps
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
export abstract class Stepper {
|
||||
done(context: Stepper.Context) {
|
||||
return context.done
|
||||
}
|
||||
|
||||
abstract step<T>(
|
||||
from: T,
|
||||
to: T,
|
||||
pos: number,
|
||||
context: Stepper.Context,
|
||||
contexts: Stepper.Context[],
|
||||
): T
|
||||
}
|
||||
|
||||
export namespace Stepper {
|
||||
export interface Context {
|
||||
done: boolean
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
export type Now = () => number
|
||||
|
||||
export type When = 'now' | 'start' | 'relative' | 'after' | 'with'
|
||||
|
||||
export interface Options {
|
||||
duration?: number
|
||||
delay?: number
|
||||
swing?: boolean
|
||||
times?: number
|
||||
wait?: number
|
||||
ease?: string
|
||||
when?: When
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import type { Dom } from '../dom'
|
||||
import type { AnimatorMap } from './types'
|
||||
import { applyMixins } from '../util/mixin'
|
||||
import { Animator } from '../animating/animator/animator'
|
||||
import { Registry } from './registry'
|
||||
|
||||
export class BaseAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>,
|
||||
TAnimator = AnimatorMap<TElement>,
|
||||
> extends Animator<TAnimator, TOwner> {
|
||||
element(): TOwner
|
||||
element(elem: TOwner): this
|
||||
element(elem?: TOwner) {
|
||||
if (elem == null) {
|
||||
return super.master()
|
||||
}
|
||||
|
||||
super.master(elem)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BaseAnimator {
|
||||
export function register(name: string) {
|
||||
return <TDefinition extends Registry.Definition>(ctor: TDefinition) => {
|
||||
Registry.register(ctor, name)
|
||||
}
|
||||
}
|
||||
|
||||
export function mixin(...source: any[]) {
|
||||
return (ctor: Registry.Definition) => {
|
||||
applyMixins(ctor, ...source)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
import { Morpher } from '../animating/morpher/morpher'
|
||||
import { MorphableObject } from '../animating/morpher/object'
|
||||
import { Dom } from '../dom'
|
||||
import { AttributesMap } from '../dom/attributes'
|
||||
import { CSSProperties } from '../dom/style'
|
||||
import { BaseAnimator } from './base'
|
||||
import { TransformAnimator } from './transform'
|
||||
|
||||
@DomAnimator.register('HTML')
|
||||
@DomAnimator.mixin(TransformAnimator)
|
||||
export class DomAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>,
|
||||
> extends BaseAnimator<TElement, TOwner> {
|
||||
attr<T extends AttributesMap<TElement>>(attrs: T): this
|
||||
attr<T extends AttributesMap<TElement>, K extends keyof T>(
|
||||
name: K,
|
||||
value: T[K],
|
||||
): this
|
||||
attr<T extends AttributesMap<TElement>, K extends keyof T>(
|
||||
name: K | T,
|
||||
value?: T[K],
|
||||
) {
|
||||
return this.queueAttrOrCSS('attr', name, value)
|
||||
}
|
||||
|
||||
css<T extends CSSProperties>(style: T): this
|
||||
css<T extends CSSProperties, K extends keyof T>(name: K, value: T[K]): this
|
||||
css<T extends CSSProperties, K extends keyof T>(name: K | T, value?: T[K]) {
|
||||
return this.queueAttrOrCSS('css', name, value)
|
||||
}
|
||||
|
||||
protected queueAttrOrCSS<
|
||||
M extends 'attr' | 'css',
|
||||
T extends M extends 'attr' ? AttributesMap<TElement> : CSSProperties,
|
||||
K extends keyof T,
|
||||
>(method: M, name: K | T, value?: T[K]): this {
|
||||
if (typeof name === 'string') {
|
||||
return this.queueAttrOrCSS(method, { [name]: value } as T)
|
||||
}
|
||||
|
||||
let attrs = name as T
|
||||
if (this.retarget(method, attrs)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<any[], T, T>(this.stepper).to(attrs)
|
||||
|
||||
let keys = Object.keys(attrs)
|
||||
|
||||
this.queue<T>(
|
||||
// prepare
|
||||
(animator) => {
|
||||
const origin = animator.element()[method](keys as any)
|
||||
morpher.from(origin as T)
|
||||
},
|
||||
|
||||
// run
|
||||
(animator, pos) => {
|
||||
const val = morpher.at(pos)
|
||||
animator.element()[method](val as any)
|
||||
return morpher.done()
|
||||
},
|
||||
|
||||
// retarget
|
||||
(animator, newToAttrs) => {
|
||||
// Check if any new keys were added
|
||||
const newKeys = Object.keys(newToAttrs)
|
||||
const diff = (a: string[], b: string[]) =>
|
||||
a.filter((x) => !b.includes(x))
|
||||
const differences = diff(newKeys, keys)
|
||||
|
||||
// If their are new keys, initialize them and add them to morpher
|
||||
if (differences.length) {
|
||||
const addedFromAttrs = animator
|
||||
.element()
|
||||
[method](differences as any) as T
|
||||
const oldFromAttrs = new MorphableObject<T>(morpher.from()).toValue()
|
||||
morpher.from({
|
||||
...oldFromAttrs,
|
||||
...addedFromAttrs,
|
||||
})
|
||||
}
|
||||
|
||||
const oldToAttrs = new MorphableObject<T>(morpher.to()).toValue()
|
||||
morpher.to({
|
||||
...oldToAttrs,
|
||||
...newToAttrs,
|
||||
})
|
||||
|
||||
// Save the work we did so we don't need it to do again
|
||||
keys = newKeys
|
||||
attrs = newToAttrs
|
||||
},
|
||||
)
|
||||
|
||||
this.remember(method, morpher)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
export interface DomAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>,
|
||||
> extends TransformAnimator<TElement, TOwner> {}
|
@ -1 +0,0 @@
|
||||
import './mixins'
|
@ -1,57 +0,0 @@
|
||||
import { applyMixins } from '../util/mixin'
|
||||
import { Dom } from '../dom/dom'
|
||||
import { Primer } from '../dom/primer/primer'
|
||||
import { Util } from '../animating/animator/util'
|
||||
import { Timeline } from '../animating/scheduler/timeline'
|
||||
import { When, Options } from '../animating/types'
|
||||
import { Registry } from './registry'
|
||||
import { AnimatorMap } from './types'
|
||||
|
||||
export class AnimateExtension<
|
||||
TElement extends Element,
|
||||
> extends Primer<TElement> {
|
||||
animate(options: Options): AnimatorMap<TElement>
|
||||
animate(duration?: number, delay?: number, when?: When): AnimatorMap<TElement>
|
||||
animate(duration?: Options | number, delay?: number, when?: When) {
|
||||
const o = Util.sanitise(duration, delay, when)
|
||||
const timeline = this.scheduler()
|
||||
const Type = Registry.get(this.node)
|
||||
return new Type(o.duration)
|
||||
.loop(o)
|
||||
.element(this as any)
|
||||
.scheduler(timeline.play())
|
||||
.schedule(o.delay, o.when)
|
||||
}
|
||||
|
||||
delay(by: number, when?: When) {
|
||||
return this.animate(0, by, when)
|
||||
}
|
||||
}
|
||||
|
||||
const cache: WeakMap<Primer, Timeline> = new WeakMap()
|
||||
|
||||
export class TimelineExtension<
|
||||
TElement extends Element = Element,
|
||||
> extends Primer<TElement> {
|
||||
scheduler(): Timeline
|
||||
scheduler(timeline: Timeline): this
|
||||
scheduler(timeline?: Timeline) {
|
||||
if (timeline == null) {
|
||||
if (!cache.has(this)) {
|
||||
cache.set(this, new Timeline())
|
||||
}
|
||||
return cache.get(this)!
|
||||
}
|
||||
|
||||
cache.set(this, timeline)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
declare module '../dom/dom' {
|
||||
interface Dom<TElement extends Element = Element>
|
||||
extends AnimateExtension<TElement>,
|
||||
TimelineExtension<TElement> {}
|
||||
}
|
||||
|
||||
applyMixins(Dom, AnimateExtension, TimelineExtension)
|
@ -1,31 +0,0 @@
|
||||
import type { BaseAnimator } from './base'
|
||||
|
||||
export namespace Registry {
|
||||
export type Definition = { new (...args: any[]): BaseAnimator }
|
||||
|
||||
const registry: Record<string, Definition> = {}
|
||||
|
||||
export function register(ctor: Definition, name: string) {
|
||||
registry[name] = ctor
|
||||
return ctor
|
||||
}
|
||||
|
||||
export function get<TClass extends Definition = Definition>(node: Node) {
|
||||
if (typeof node === 'string') {
|
||||
return registry[node] as TClass
|
||||
}
|
||||
|
||||
const nodeName = node.nodeName
|
||||
let className = nodeName[0].toUpperCase() + nodeName.substring(1)
|
||||
|
||||
if (node instanceof SVGElement) {
|
||||
if (registry[className] == null) {
|
||||
className = 'SVG'
|
||||
}
|
||||
} else {
|
||||
className = 'HTML'
|
||||
}
|
||||
|
||||
return registry[className] as TClass
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import { Vector } from '../vector/vector/vector'
|
||||
import { Morpher } from '../animating/morpher/morpher'
|
||||
import { Morphable } from '../animating/morpher/morphable'
|
||||
import { MorphableUnitNumber } from '../animating/morpher/unit-number'
|
||||
import { DomAnimator } from './dom'
|
||||
|
||||
@SVGAnimator.register('SVG')
|
||||
export class SVGAnimator<
|
||||
TSVGElement extends SVGElement = SVGElement,
|
||||
TOwner extends Vector<TSVGElement> = Vector<TSVGElement>,
|
||||
> extends DomAnimator<TSVGElement, TOwner> {
|
||||
x(x: number | string) {
|
||||
return this.queueNumber('x', x)
|
||||
}
|
||||
|
||||
y(y: number | string) {
|
||||
return this.queueNumber('y', y)
|
||||
}
|
||||
|
||||
move(x: number | string = 0, y: number | string = 0) {
|
||||
return this.x(x).y(y)
|
||||
}
|
||||
|
||||
dx(x: number | string) {
|
||||
return this.queueDelta('x', x)
|
||||
}
|
||||
|
||||
dy(y: number | string) {
|
||||
return this.queueDelta('y', y)
|
||||
}
|
||||
|
||||
dmove(x: number | string = 0, y: number | string = 0) {
|
||||
return this.dx(x).dy(y)
|
||||
}
|
||||
|
||||
cx(x: number | string) {
|
||||
return this.queueNumber('cx', x)
|
||||
}
|
||||
|
||||
cy(y: number | string) {
|
||||
return this.queueNumber('cy', y)
|
||||
}
|
||||
|
||||
center(x: number | string = 0, y: number | string = 0) {
|
||||
return this.cx(x).cy(y)
|
||||
}
|
||||
|
||||
width(width: number | string) {
|
||||
return this.queueNumber('width', width)
|
||||
}
|
||||
|
||||
height(height: number | string) {
|
||||
return this.queueNumber('height', height)
|
||||
}
|
||||
|
||||
size(width?: number | string, height?: number | string) {
|
||||
if (width == null && height == null) {
|
||||
return this
|
||||
}
|
||||
|
||||
let w = MorphableUnitNumber.toNumber(width!)
|
||||
let h = MorphableUnitNumber.toNumber(height!)
|
||||
|
||||
if (width == null || height == null) {
|
||||
const box = this.element().bbox()
|
||||
|
||||
if (!width) {
|
||||
w = (box.width / box.height) * h
|
||||
}
|
||||
|
||||
if (!height) {
|
||||
h = (box.height / box.width) * w
|
||||
}
|
||||
}
|
||||
|
||||
return this.width(w).height(h)
|
||||
}
|
||||
|
||||
protected queueDelta(method: 'x' | 'y', to: number | string) {
|
||||
if (this.retarget(method, to)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<number[], number | string, number>(
|
||||
this.stepper,
|
||||
).to(to)
|
||||
let from: number
|
||||
this.queue<string | number>(
|
||||
(animator) => {
|
||||
from = animator.element()[method]()
|
||||
morpher.from(from)
|
||||
morpher.to(from + MorphableUnitNumber.toNumber(to))
|
||||
},
|
||||
(animator, pos) => {
|
||||
animator.element()[method](morpher.at(pos))
|
||||
return morpher.done()
|
||||
},
|
||||
(animator, newTo) => {
|
||||
morpher.to(from + MorphableUnitNumber.toNumber(newTo))
|
||||
},
|
||||
)
|
||||
|
||||
this.remember(method, morpher)
|
||||
return this
|
||||
}
|
||||
|
||||
protected queueNumber(
|
||||
method: 'x' | 'y' | 'cx' | 'cy' | 'width' | 'height' | 'leading',
|
||||
value: number | string,
|
||||
) {
|
||||
return this.queueObject(method, new MorphableUnitNumber(value))
|
||||
}
|
||||
|
||||
protected queueObject(method: string, to: Morphable<any[], any>) {
|
||||
if (this.retarget(method, to)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<any[], any, any>(this.stepper).to(to)
|
||||
this.queue<any>(
|
||||
(animator) => {
|
||||
morpher.from(animator.element()[method as 'x']())
|
||||
},
|
||||
(animator, pos) => {
|
||||
animator.element()[method as 'x'](morpher.at(pos))
|
||||
return morpher.done()
|
||||
},
|
||||
)
|
||||
|
||||
this.remember(method, morpher)
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
import { Dom } from '../../dom'
|
||||
import { Point } from '../../struct/point'
|
||||
import { Matrix } from '../../struct/matrix'
|
||||
import { BaseAnimator } from '../base'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphableMatrix } from '../../animating/morpher/matrix'
|
||||
import { MorphableTransform } from '../../animating/morpher/transform'
|
||||
import { getTransformOrigin } from '../../dom/transform/util'
|
||||
import * as Util from './util'
|
||||
|
||||
export class TransformAnimator<
|
||||
TElement extends Element = Element,
|
||||
TOwner extends Dom<TElement> = Dom<TElement>,
|
||||
> extends BaseAnimator<TElement, TOwner> {
|
||||
transform(
|
||||
transforms: Matrix.MatrixLike | Matrix.TransformOptions,
|
||||
relative?: boolean,
|
||||
affine?: boolean,
|
||||
) {
|
||||
//
|
||||
// M v -----|-----(D M v = F v)------|-----> T v
|
||||
//
|
||||
// 1. define the final state (T) and decompose it (once)
|
||||
// t = [tx, ty, the, lam, sy, sx]
|
||||
// 2. on every frame: pull the current state of all previous transforms
|
||||
// (M - m can change)
|
||||
// and then write this as m = [tx0, ty0, the0, lam0, sy0, sx0]
|
||||
// 3. Find the interpolated matrix F(pos) = m + pos * (t - m)
|
||||
// - Note F(0) = M
|
||||
// - Note F(1) = T
|
||||
// 4. Now you get the delta matrix as a result: D = F * inv(M)
|
||||
|
||||
Util.prepareTransform(this.element())
|
||||
|
||||
const method = 'transform'
|
||||
|
||||
// If we have a declarative function, we should retarget it if possible
|
||||
if (this.declarative && !relative) {
|
||||
if (this.retarget(method, transforms)) {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const isMatrix = MorphableMatrix.isMatrixLike(transforms)
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
affine = affine != null ? affine : !isMatrix
|
||||
|
||||
const morpher = new Morpher<
|
||||
Matrix.MatrixArray,
|
||||
Matrix.TransformOptions,
|
||||
Matrix.TransformOptions
|
||||
>(this.stepper).type(affine ? MorphableTransform : MorphableMatrix)
|
||||
|
||||
let origin: [number, number]
|
||||
let element: TOwner
|
||||
let startTransformMatrix: Matrix
|
||||
let currentTransformMatrix: Matrix
|
||||
let currentAngle: number
|
||||
|
||||
this.queue<Matrix.TransformOptions>(
|
||||
// prepare
|
||||
(animator) => {
|
||||
// make sure element and origin is defined
|
||||
element = element || animator.element()
|
||||
origin =
|
||||
origin ||
|
||||
getTransformOrigin(transforms as Matrix.TransformOptions, element)
|
||||
|
||||
startTransformMatrix = new Matrix(relative ? undefined : element)
|
||||
|
||||
// add the animator to the element so it can merge transformations
|
||||
Util.addAnimator(element, animator)
|
||||
|
||||
// Deactivate all transforms that have run so far if we are absolute
|
||||
if (!relative) {
|
||||
Util.clearAnimatorsBefore(element, animator).forEach((animator) => {
|
||||
if (animator instanceof BaseAnimator) {
|
||||
if (!animator.done) {
|
||||
const timeline = animator.scheduler()
|
||||
if (timeline == null || !timeline.has(animator)) {
|
||||
const a = animator as any as TransformAnimator
|
||||
a.clearTransformExecutors()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
(animator, pos) => {
|
||||
if (!relative) {
|
||||
Util.clearTransform(animator)
|
||||
}
|
||||
|
||||
const ctm = Util.getCurrentTransform(element, animator)
|
||||
const { x, y } = new Point(origin[0], origin[1]).transform(ctm)
|
||||
const target = new Matrix({ ...transforms, origin: [x, y] })
|
||||
const source =
|
||||
animator.declarative && currentTransformMatrix
|
||||
? currentTransformMatrix
|
||||
: startTransformMatrix
|
||||
|
||||
const t = target.decompose(x, y)
|
||||
const s = source.decompose(x, y)
|
||||
|
||||
if (affine) {
|
||||
// Get the current and target angle as it was set
|
||||
const rt = t.rotate
|
||||
const rs = s.rotate
|
||||
|
||||
// Figure out the shortest path to rotate directly
|
||||
const possibilities = [rt - 360, rt, rt + 360]
|
||||
const distances = possibilities.map((a) => Math.abs(a - rs))
|
||||
const shortest = Math.min(...distances)
|
||||
const index = distances.indexOf(shortest)
|
||||
t.rotate = possibilities[index]
|
||||
}
|
||||
|
||||
if (relative) {
|
||||
// we have to be careful here not to overwrite the rotation
|
||||
// with the rotate method of Matrix
|
||||
if (!isMatrix) {
|
||||
t.rotate = (transforms as Matrix.TransformOptions).rotate || 0
|
||||
}
|
||||
if (this.declarative && currentAngle) {
|
||||
s.rotate = currentAngle
|
||||
}
|
||||
}
|
||||
|
||||
morpher.from(s)
|
||||
morpher.to(t)
|
||||
|
||||
const affineParameters = morpher.at(pos)
|
||||
currentAngle = affineParameters.rotate!
|
||||
currentTransformMatrix = new Matrix(affineParameters)
|
||||
|
||||
Util.addTransform(animator, currentTransformMatrix)
|
||||
Util.addAnimator(element, animator)
|
||||
|
||||
return morpher.done()
|
||||
},
|
||||
|
||||
// retarget
|
||||
(animator, newTransforms) => {
|
||||
const prev = (transforms as Matrix.TransformOptions).origin || 'center'
|
||||
const next = (newTransforms.origin || 'center').toString()
|
||||
|
||||
// only get a new origin if it changed since the last call
|
||||
if (prev.toString() !== next.toString()) {
|
||||
origin = getTransformOrigin(newTransforms, element)
|
||||
}
|
||||
|
||||
// overwrite the old transformations with the new ones
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
transforms = { ...newTransforms, origin }
|
||||
},
|
||||
|
||||
true,
|
||||
)
|
||||
|
||||
if (this.declarative) {
|
||||
this.remember(method, morpher)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
protected clearTransformExecutors() {
|
||||
const executors = this.executors
|
||||
for (let i = executors.length - 1; i >= 0; i -= 1) {
|
||||
if (executors[i].isTransform) {
|
||||
executors.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
import { DomAnimator } from '../dom'
|
||||
import { MockedAnimator } from './mock'
|
||||
|
||||
export class AnimatorList {
|
||||
public readonly ids: number[] = []
|
||||
public readonly animators: (DomAnimator | MockedAnimator)[] = []
|
||||
|
||||
add(animator: DomAnimator | MockedAnimator) {
|
||||
if (this.animators.includes(animator)) {
|
||||
return this
|
||||
}
|
||||
|
||||
this.ids.push(animator.id)
|
||||
this.animators.push(animator)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
const index = this.ids.indexOf(id)
|
||||
this.ids.splice(index, 1)
|
||||
this.animators.splice(index, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
replace(id: number, animator: DomAnimator | MockedAnimator) {
|
||||
const index = this.ids.indexOf(id)
|
||||
this.ids.splice(index, 1, id)
|
||||
this.animators.splice(index, 1, animator)
|
||||
return this
|
||||
}
|
||||
|
||||
get(id: number) {
|
||||
return this.animators[this.ids.indexOf(id)]
|
||||
}
|
||||
|
||||
length() {
|
||||
return this.ids.length
|
||||
}
|
||||
|
||||
clearBefore(id: number) {
|
||||
const deleteCount = this.ids.indexOf(id) || 1
|
||||
this.ids.splice(0, deleteCount, 0)
|
||||
this.animators.splice(0, deleteCount, new MockedAnimator())
|
||||
return this.animators
|
||||
}
|
||||
|
||||
merge(
|
||||
mergeFn: (
|
||||
a1: DomAnimator | MockedAnimator,
|
||||
a2: DomAnimator | MockedAnimator,
|
||||
) => MockedAnimator,
|
||||
) {
|
||||
let prev: DomAnimator | MockedAnimator | null = null
|
||||
for (let i = 0; i < this.animators.length; i += 1) {
|
||||
const curr = this.animators[i]
|
||||
const currs = curr.scheduler()
|
||||
const prevs = prev && prev.scheduler()
|
||||
|
||||
const needMerge =
|
||||
prev != null &&
|
||||
prev.done &&
|
||||
curr.done &&
|
||||
// don't merge animator when persisted on timeline
|
||||
(currs == null || !currs.has(curr.id)) &&
|
||||
(prevs == null || !prevs.has(prev.id))
|
||||
|
||||
if (needMerge) {
|
||||
this.remove(curr.id)
|
||||
const merged = mergeFn(curr, prev!)
|
||||
this.replace(prev!.id, merged)
|
||||
prev = merged
|
||||
i -= 1
|
||||
} else {
|
||||
prev = curr
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export class MockedAnimator {
|
||||
constructor(public id = -1, public done = true) {}
|
||||
scheduler() {}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
import { Dom } from '../../dom'
|
||||
import { Matrix } from '../../struct/matrix'
|
||||
import { Queue } from '../../animating/scheduler/queue'
|
||||
import { Timing } from '../../animating/scheduler/timing'
|
||||
import { BaseAnimator } from '../base'
|
||||
import { AnimatorList } from './list'
|
||||
import { MockedAnimator } from './mock'
|
||||
|
||||
const animators: WeakMap<Dom, AnimatorList> = new WeakMap()
|
||||
const frames: WeakMap<Dom, Queue.Item<Timing.Frame>> = new WeakMap()
|
||||
|
||||
export function prepareTransform(elem: Dom) {
|
||||
if (!frames.has(elem)) {
|
||||
const mock = createMockAnimator(new Matrix(elem))
|
||||
const list = new AnimatorList().add(mock)
|
||||
animators.set(elem, list)
|
||||
}
|
||||
}
|
||||
|
||||
function createMockAnimator(matrix?: Matrix, id?: number) {
|
||||
const mock = new MockedAnimator(id)
|
||||
addTransform(mock, matrix)
|
||||
return mock
|
||||
}
|
||||
|
||||
export function addAnimator(elem: Dom, animator: BaseAnimator) {
|
||||
animators.get(elem)!.add(animator)
|
||||
|
||||
let frameId = frames.get(elem) || null
|
||||
|
||||
// Make sure that the animator merge is executed at the very end of
|
||||
// all animation functions. Thats why we use immediate here to execute
|
||||
// the merge right after all frames are run
|
||||
Timing.cancelImmediate(frameId)
|
||||
|
||||
frameId = Timing.immediate(() => {
|
||||
const list = animators.get(elem)!
|
||||
const next = list.animators
|
||||
.map((animator) => getTransform(animator)!)
|
||||
.reduce((memo, curr) => memo.lmultiplyO(curr), new Matrix())
|
||||
|
||||
elem.transform(next)
|
||||
list.merge((a1, a2) => {
|
||||
const m1 = getTransform(a1)!
|
||||
const m2 = getTransform(a2)!
|
||||
clearTransform(a2)
|
||||
return createMockAnimator(m2.lmultiply(m1), a2.id)
|
||||
})
|
||||
|
||||
if (list.length() === 1) {
|
||||
frames.delete(elem)
|
||||
}
|
||||
})
|
||||
|
||||
frames.set(elem, frameId)
|
||||
}
|
||||
|
||||
export function clearAnimatorsBefore(elem: Dom, animator: BaseAnimator) {
|
||||
const cache = animators.get(elem)!
|
||||
return cache.clearBefore(animator.id)
|
||||
}
|
||||
|
||||
export function getCurrentTransform(elem: Dom, animator: BaseAnimator) {
|
||||
const arr = animators.get(elem)!
|
||||
|
||||
return (
|
||||
arr.animators
|
||||
// we need the equal sign here to make sure, that also transformations
|
||||
// on the same runner which execute before the current transformation are
|
||||
// taken into account
|
||||
.filter((item) => item.id <= animator.id)
|
||||
.map((animator) => getTransform(animator)!)
|
||||
.reduce((memo, curr) => memo.lmultiplyO(curr), new Matrix())
|
||||
)
|
||||
}
|
||||
|
||||
const transforms: WeakMap<BaseAnimator | MockedAnimator, Matrix> = new WeakMap()
|
||||
|
||||
export function addTransform(
|
||||
animator: BaseAnimator | MockedAnimator,
|
||||
transform?: Matrix.Raw,
|
||||
) {
|
||||
let ctm = transforms.get(animator)
|
||||
if (ctm == null) {
|
||||
ctm = new Matrix()
|
||||
}
|
||||
|
||||
if (transform) {
|
||||
ctm.lmultiplyO(transform)
|
||||
}
|
||||
|
||||
transforms.set(animator, ctm)
|
||||
}
|
||||
|
||||
export function clearTransform(animator: BaseAnimator | MockedAnimator) {
|
||||
transforms.set(animator, new Matrix())
|
||||
}
|
||||
|
||||
export function getTransform(animator: BaseAnimator | MockedAnimator) {
|
||||
return transforms.get(animator)
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { DomAnimator } from './dom'
|
||||
import { SVGAnimator } from './svg'
|
||||
import { SVGAAnimator } from './vector/a'
|
||||
import { SVGClipPathAnimator } from './vector/clippath'
|
||||
import { SVGGAnimator } from './vector/g'
|
||||
import { SVGDefsAnimator } from './vector/defs'
|
||||
import { SVGGradientAnimator } from './vector/gradient'
|
||||
import { SVGMarkerAnimator } from './vector/marker'
|
||||
import { SVGMaskAnimator } from './vector/mask'
|
||||
import { SVGPatternAnimator } from './vector/pattern'
|
||||
import { SVGSVGAnimator } from './vector/svg'
|
||||
import { SVGSymbolAnimator } from './vector/symbol'
|
||||
import { SVGCircleAnimator } from './vector/circle'
|
||||
import { SVGEllipseAnimator } from './vector/ellipse'
|
||||
import { SVGImageAnimator } from './vector/image'
|
||||
import { SVGLineAnimator } from './vector/line'
|
||||
import { SVGPolygonAnimator } from './vector/polygon'
|
||||
import { SVGPolylineAnimator } from './vector/polyline'
|
||||
import { SVGRectAnimator } from './vector/rect'
|
||||
import { SVGStyleAnimator } from './vector/style'
|
||||
import { SVGTextAnimator } from './vector/text'
|
||||
import { SVGTSpanAnimator } from './vector/tspan'
|
||||
import { SVGUseAnimator } from './vector/use'
|
||||
import { SVGPathAnimator } from './vector/path'
|
||||
import { SVGForeignObjectAnimator } from './vector/foreignobject'
|
||||
import { SVGLinearGradientAnimator } from './vector/linear-gradient'
|
||||
import { SVGRadialGradientAnimator } from './vector/radial-gradient'
|
||||
|
||||
// prettier-ignore
|
||||
export type AnimatorMap<T> =
|
||||
T extends SVGAElement ? SVGAAnimator
|
||||
: T extends SVGLineElement ? SVGLineAnimator
|
||||
: T extends SVGPathElement ? SVGPathAnimator
|
||||
: T extends SVGCircleElement ? SVGCircleAnimator
|
||||
: T extends SVGClipPathElement ? SVGClipPathAnimator
|
||||
: T extends SVGEllipseElement ? SVGEllipseAnimator
|
||||
: T extends SVGImageElement ? SVGImageAnimator
|
||||
: T extends SVGRectElement ? SVGRectAnimator
|
||||
: T extends SVGUseElement ? SVGUseAnimator
|
||||
: T extends SVGSVGElement ? SVGSVGAnimator
|
||||
: T extends SVGForeignObjectElement ? SVGForeignObjectAnimator
|
||||
: T extends SVGTSpanElement ? SVGTSpanAnimator
|
||||
: T extends SVGTextElement ? SVGTextAnimator
|
||||
: T extends SVGPolylineElement ? SVGPolylineAnimator
|
||||
: T extends SVGPolygonElement ? SVGPolygonAnimator
|
||||
: T extends SVGGElement ? SVGGAnimator
|
||||
: T extends SVGDefsElement ? SVGDefsAnimator
|
||||
: T extends SVGGradientElement ? SVGGradientAnimator<T>
|
||||
: T extends SVGLinearGradientElement ? SVGLinearGradientAnimator
|
||||
: T extends SVGRadialGradientElement ? SVGRadialGradientAnimator
|
||||
: T extends SVGMarkerElement ? SVGMarkerAnimator
|
||||
: T extends SVGMaskElement ? SVGMaskAnimator
|
||||
: T extends SVGPatternElement ? SVGPatternAnimator
|
||||
: T extends SVGPolygonElement ? SVGPolygonAnimator
|
||||
: T extends SVGPolylineElement ? SVGPolylineAnimator
|
||||
: T extends SVGSymbolElement ? SVGSymbolAnimator
|
||||
: T extends SVGStyleElement ? SVGStyleAnimator
|
||||
: T extends SVGElement ? SVGAnimator<T>
|
||||
|
||||
: T extends Element ? DomAnimator<T>
|
||||
: DomAnimator<any>
|
@ -1,8 +0,0 @@
|
||||
import { A } from '../../vector/a/a'
|
||||
import { SVGContainerGeometryAnimator } from './container-geometry'
|
||||
|
||||
@SVGAAnimator.register('A')
|
||||
export class SVGAAnimator extends SVGContainerGeometryAnimator<
|
||||
SVGAElement,
|
||||
A
|
||||
> {}
|
@ -1,5 +0,0 @@
|
||||
import { Circle } from '../../vector/circle/circle'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGCircleAnimator.register('Circle')
|
||||
export class SVGCircleAnimator extends SVGAnimator<SVGCircleElement, Circle> {}
|
@ -1,8 +0,0 @@
|
||||
import { ClipPath } from '../../vector/clippath/clippath'
|
||||
import { SVGWrapperAnimator } from './wrapper'
|
||||
|
||||
@SVGClipPathAnimator.register('ClipPath')
|
||||
export class SVGClipPathAnimator extends SVGWrapperAnimator<
|
||||
SVGClipPathElement,
|
||||
ClipPath
|
||||
> {}
|
@ -1,7 +0,0 @@
|
||||
import { ContainerGeometry } from '../../vector/container/geometry'
|
||||
import { SVGContainerAnimator } from './container'
|
||||
|
||||
export class SVGContainerGeometryAnimator<
|
||||
TSVGElement extends SVGAElement | SVGGElement,
|
||||
TOwner extends ContainerGeometry<TSVGElement> = ContainerGeometry<TSVGElement>,
|
||||
> extends SVGContainerAnimator<TSVGElement, TOwner> {}
|
@ -1,44 +0,0 @@
|
||||
import { Point } from '../../struct/point'
|
||||
import { Viewbox } from '../../vector/container/viewbox'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphableBox } from '../../animating/morpher/box'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGViewboxAnimator<
|
||||
TSVGElement extends
|
||||
| SVGSVGElement
|
||||
| SVGSymbolElement
|
||||
| SVGPatternElement
|
||||
| SVGMarkerElement,
|
||||
TOwner extends Viewbox<TSVGElement> = Viewbox<TSVGElement>,
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {
|
||||
zoom(level: number, point: Point.PointLike) {
|
||||
if (this.retarget('zoom', level, point)) {
|
||||
return this
|
||||
}
|
||||
const origin = { x: point.x, y: point.y }
|
||||
const morpher = new Morpher<[number], number, number>(this.stepper).to(
|
||||
level,
|
||||
)
|
||||
this.queue<number, Point.PointLike>(
|
||||
(animator) => {
|
||||
morpher.from(animator.element().zoom())
|
||||
},
|
||||
(animator, pos) => {
|
||||
animator.element().zoom(morpher.at(pos), origin)
|
||||
return morpher.done()
|
||||
},
|
||||
(animator, newLevel, newPoint) => {
|
||||
origin.x = newPoint.x
|
||||
origin.y = newPoint.y
|
||||
morpher.to(newLevel)
|
||||
},
|
||||
)
|
||||
this.remember('zoom', morpher)
|
||||
return this
|
||||
}
|
||||
|
||||
viewbox(x: number, y: number, width: number, height: number) {
|
||||
return this.queueObject('viewbox', new MorphableBox(x, y, width, height))
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { Container } from '../../vector/container/container'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGContainerAnimator<
|
||||
TSVGElement extends SVGElement,
|
||||
TOwner extends Container<TSVGElement> = Container<TSVGElement>,
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {}
|
@ -1,5 +0,0 @@
|
||||
import { Defs } from '../../vector/defs/defs'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGDefsAnimator.register('Defs')
|
||||
export class SVGDefsAnimator extends SVGAnimator<SVGDefsElement, Defs> {}
|
@ -1,8 +0,0 @@
|
||||
import { Ellipse } from '../../vector/ellipse/ellipse'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGEllipseAnimator.register('Ellipse')
|
||||
export class SVGEllipseAnimator extends SVGAnimator<
|
||||
SVGEllipseElement,
|
||||
Ellipse
|
||||
> {}
|
@ -1,8 +0,0 @@
|
||||
import { ForeignObject } from '../../vector/foreignobject/foreignobject'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGForeignObjectAnimator.register('ForeignObject')
|
||||
export class SVGForeignObjectAnimator extends SVGAnimator<
|
||||
SVGForeignObjectElement,
|
||||
ForeignObject
|
||||
> {}
|
@ -1,8 +0,0 @@
|
||||
import { G } from '../../vector/g/g'
|
||||
import { SVGContainerGeometryAnimator } from './container-geometry'
|
||||
|
||||
@SVGGAnimator.register('G')
|
||||
export class SVGGAnimator extends SVGContainerGeometryAnimator<
|
||||
SVGGElement,
|
||||
G
|
||||
> {}
|
@ -1,7 +0,0 @@
|
||||
import { Gradient } from '../../vector/gradient/gradient'
|
||||
import { SVGWrapperAnimator } from './wrapper'
|
||||
|
||||
export class SVGGradientAnimator<
|
||||
TSVGElement extends SVGGradientElement,
|
||||
TOwner extends Gradient<TSVGElement> = Gradient<TSVGElement>,
|
||||
> extends SVGWrapperAnimator<TSVGElement, TOwner> {}
|
@ -1,5 +0,0 @@
|
||||
import { Image } from '../../vector/image/image'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGImageAnimator.register('Image')
|
||||
export class SVGImageAnimator extends SVGAnimator<SVGImageElement, Image> {}
|
@ -1,55 +0,0 @@
|
||||
import { Line } from '../../vector/line/line'
|
||||
import { PointArray } from '../../struct/point-array'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphablePointArray } from '../../animating/morpher/point-array'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGLineAnimator.register('Line')
|
||||
export class SVGLineAnimator extends SVGAnimator<SVGLineElement, Line> {
|
||||
plot(d: string): this
|
||||
plot(points: Line.Array | PointArray): this
|
||||
plot(x1: number, y1: number, x2: number, y2: number): this
|
||||
plot(
|
||||
x1: Line.Array | PointArray | number | string,
|
||||
y1?: number,
|
||||
x2?: number,
|
||||
y2?: number,
|
||||
): this
|
||||
plot(
|
||||
x1: Line.Array | PointArray | number | string,
|
||||
y1?: number,
|
||||
x2?: number,
|
||||
y2?: number,
|
||||
) {
|
||||
if (typeof x1 === 'number') {
|
||||
return this.plot([
|
||||
[x1, y1!],
|
||||
[x2!, y2!],
|
||||
])
|
||||
}
|
||||
|
||||
if (this.retarget('plot', x1)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<
|
||||
Line.Array,
|
||||
Line.Array | PointArray | string,
|
||||
Line.Array
|
||||
>(this.stepper)
|
||||
.type(MorphablePointArray)
|
||||
.to(x1)
|
||||
|
||||
this.queue<Line.Array>(
|
||||
(animator) => {
|
||||
morpher.from(animator.element().toArray())
|
||||
},
|
||||
(animator, pos) => {
|
||||
animator.plot(morpher.at(pos))
|
||||
return morpher.done()
|
||||
},
|
||||
)
|
||||
this.remember('plot', morpher)
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { LinearGradient } from '../../vector/gradient/linear'
|
||||
import { SVGGradientAnimator } from './gradient'
|
||||
|
||||
@SVGLinearGradientAnimator.register('LinearGradient')
|
||||
export class SVGLinearGradientAnimator extends SVGGradientAnimator<
|
||||
SVGLinearGradientElement,
|
||||
LinearGradient
|
||||
> {}
|
@ -1,8 +0,0 @@
|
||||
import { Marker } from '../../vector/marker/marker'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGMarkerAnimator.register('Marker')
|
||||
export class SVGMarkerAnimator extends SVGViewboxAnimator<
|
||||
SVGMarkerElement,
|
||||
Marker
|
||||
> {}
|
@ -1,5 +0,0 @@
|
||||
import { Mask } from '../../vector/mask/mask'
|
||||
import { SVGWrapperAnimator } from './wrapper'
|
||||
|
||||
@SVGMaskAnimator.register('Mask')
|
||||
export class SVGMaskAnimator extends SVGWrapperAnimator<SVGMaskElement, Mask> {}
|
@ -1,37 +0,0 @@
|
||||
import { Path } from '../../vector/path/path'
|
||||
import { PathArray } from '../../struct/path-array'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { MorphablePathArray } from '../../animating/morpher/path-array'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGPathAnimator.register('Path')
|
||||
export class SVGPathAnimator extends SVGAnimator<SVGPathElement, Path> {
|
||||
plot(d: string): this
|
||||
plot(segments: Path.Segment[]): this
|
||||
plot(pathArray: PathArray): this
|
||||
plot(d: string | Path.Segment[] | PathArray) {
|
||||
if (this.retarget('plot', d)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<
|
||||
Path.Segment[],
|
||||
string | Path.Segment[] | PathArray,
|
||||
Path.Segment[]
|
||||
>(this.stepper)
|
||||
.type(MorphablePathArray)
|
||||
.to(d)
|
||||
|
||||
this.queue<Path.Segment[]>(
|
||||
(animator) => {
|
||||
morpher.from(animator.element().toArray())
|
||||
},
|
||||
(animator, pos) => {
|
||||
animator.plot(morpher.at(pos))
|
||||
return morpher.done()
|
||||
},
|
||||
)
|
||||
this.remember('plot', morpher)
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { Pattern } from '../../vector/pattern/pattern'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGPatternAnimator.register('Pattern')
|
||||
export class SVGPatternAnimator extends SVGViewboxAnimator<
|
||||
SVGPatternElement,
|
||||
Pattern
|
||||
> {}
|
@ -1,38 +0,0 @@
|
||||
import { Poly } from '../../vector/poly/poly'
|
||||
import { MorphablePointArray } from '../../animating/morpher/point-array'
|
||||
import { Morpher } from '../../animating/morpher/morpher'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGPolyAnimator<
|
||||
TSVGElement extends SVGPolygonElement | SVGPolylineElement,
|
||||
TOwner extends Poly<TSVGElement> = Poly<TSVGElement>,
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {
|
||||
plot(d: string): this
|
||||
plot(points: [number, number][]): this
|
||||
plot(points: string | [number, number][]): this
|
||||
plot(d: string | [number, number][]) {
|
||||
if (this.retarget('plot', d)) {
|
||||
return this
|
||||
}
|
||||
|
||||
const morpher = new Morpher<
|
||||
[number, number][],
|
||||
string | [number, number][],
|
||||
[number, number][]
|
||||
>(this.stepper)
|
||||
.type(MorphablePointArray)
|
||||
.to(d)
|
||||
|
||||
this.queue<[number, number][]>(
|
||||
(animator) => {
|
||||
morpher.from(animator.element().toArray())
|
||||
},
|
||||
(animator, pos) => {
|
||||
animator.plot(morpher.at(pos))
|
||||
return morpher.done()
|
||||
},
|
||||
)
|
||||
this.remember('plot', morpher)
|
||||
return this
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { Polygon } from '../../vector/polygon/polygon'
|
||||
import { SVGPolyAnimator } from './poly'
|
||||
|
||||
@SVGPolygonAnimator.register('Polygon')
|
||||
export class SVGPolygonAnimator extends SVGPolyAnimator<
|
||||
SVGPolygonElement,
|
||||
Polygon
|
||||
> {}
|
@ -1,8 +0,0 @@
|
||||
import { Polyline } from '../../vector/polyline/polyline'
|
||||
import { SVGPolyAnimator } from './poly'
|
||||
|
||||
@SVGPolylineAnimator.register('Polyline')
|
||||
export class SVGPolylineAnimator extends SVGPolyAnimator<
|
||||
SVGPolylineElement,
|
||||
Polyline
|
||||
> {}
|
@ -1,8 +0,0 @@
|
||||
import { RadialGradient } from '../../vector/gradient/radial'
|
||||
import { SVGGradientAnimator } from './gradient'
|
||||
|
||||
@SVGRadialGradientAnimator.register('RadialGradient')
|
||||
export class SVGRadialGradientAnimator extends SVGGradientAnimator<
|
||||
SVGRadialGradientElement,
|
||||
RadialGradient
|
||||
> {}
|
@ -1,5 +0,0 @@
|
||||
import { Rect } from '../../vector/rect/rect'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGRectAnimator.register('Rect')
|
||||
export class SVGRectAnimator extends SVGAnimator<SVGRectElement, Rect> {}
|
@ -1,5 +0,0 @@
|
||||
import { Style } from '../../vector/style/style'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGStyleAnimator.register('Style')
|
||||
export class SVGStyleAnimator extends SVGAnimator<SVGStyleElement, Style> {}
|
@ -1,5 +0,0 @@
|
||||
import { Svg } from '../../vector/svg/svg'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGSVGAnimator.register('Svg')
|
||||
export class SVGSVGAnimator extends SVGViewboxAnimator<SVGSVGElement, Svg> {}
|
@ -1,8 +0,0 @@
|
||||
import { Symbol } from '../../vector/symbol/symbol'
|
||||
import { SVGViewboxAnimator } from './container-viewbox'
|
||||
|
||||
@SVGSymbolAnimator.register('Symbol')
|
||||
export class SVGSymbolAnimator extends SVGViewboxAnimator<
|
||||
SVGSymbolElement,
|
||||
Symbol // eslint-disable-line
|
||||
> {}
|
@ -1,9 +0,0 @@
|
||||
import { Text } from '../../vector/text/text'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGTextAnimator.register('Text')
|
||||
export class SVGTextAnimator extends SVGAnimator<SVGTextElement, Text> {
|
||||
leading(value: number | string) {
|
||||
return this.queueNumber('leading', value)
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { TSpan } from '../../vector/tspan/tspan'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGTSpanAnimator.register('Tspan')
|
||||
export class SVGTSpanAnimator extends SVGAnimator<SVGTSpanElement, TSpan> {}
|
@ -1,5 +0,0 @@
|
||||
import { Use } from '../../vector/use/use'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
@SVGUseAnimator.register('Use')
|
||||
export class SVGUseAnimator extends SVGAnimator<SVGUseElement, Use> {}
|
@ -1,7 +0,0 @@
|
||||
import { Wrapper } from '../../vector/container/wrapper'
|
||||
import { SVGAnimator } from '../svg'
|
||||
|
||||
export class SVGWrapperAnimator<
|
||||
TSVGElement extends SVGElement,
|
||||
TOwner extends Wrapper<TSVGElement> = Wrapper<TSVGElement>,
|
||||
> extends SVGAnimator<TSVGElement, TOwner> {}
|
@ -1,5 +1,5 @@
|
||||
import { createHTMLNode, createSVGNode } from '../../util/dom'
|
||||
import { G, Svg } from '../../vector'
|
||||
import { G, SVG } from '../../vector'
|
||||
import { Dom } from '../dom'
|
||||
import { Adopter } from './adopter'
|
||||
|
||||
@ -46,9 +46,9 @@ describe('Adopter', () => {
|
||||
|
||||
describe('makeInstance()', () => {
|
||||
it('should create a svg instance with nil arg', () => {
|
||||
expect(Adopter.makeInstance()).toBeInstanceOf(Svg)
|
||||
expect(Adopter.makeInstance(null)).toBeInstanceOf(Svg)
|
||||
expect(Adopter.makeInstance(undefined)).toBeInstanceOf(Svg)
|
||||
expect(Adopter.makeInstance()).toBeInstanceOf(SVG)
|
||||
expect(Adopter.makeInstance(null)).toBeInstanceOf(SVG)
|
||||
expect(Adopter.makeInstance(undefined)).toBeInstanceOf(SVG)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -3,7 +3,7 @@ import { Global } from '../../global'
|
||||
import { Registry } from './registry'
|
||||
import type { Base } from './base'
|
||||
import type { Dom } from '../dom'
|
||||
import type { Svg } from '../../vector'
|
||||
import type { SVG } from '../../vector'
|
||||
import type { ElementMap } from '../../types'
|
||||
import type { HTMLAttributesTagNameMap } from '../types'
|
||||
import type { SVGAttributesTagNameMap } from '../../vector/types'
|
||||
@ -47,8 +47,8 @@ export namespace Adopter {
|
||||
|
||||
export type Target<T extends Dom = Dom> = T | Node | string
|
||||
|
||||
export function makeInstance(): Svg
|
||||
export function makeInstance(node: undefined | null): Svg
|
||||
export function makeInstance(): SVG
|
||||
export function makeInstance(node: undefined | null): SVG
|
||||
export function makeInstance<T extends Dom>(instance: T): T
|
||||
export function makeInstance<T extends Dom>(target: Target<T>): T
|
||||
export function makeInstance<T extends Node>(node: T): ElementMap<T>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import sinon from 'sinon'
|
||||
import { createSVGNode, namespaces } from '../util/dom'
|
||||
import { Circle, G, Rect, Svg, TSpan } from '../vector'
|
||||
import { Circle, G, Rect, SVG, TSpan } from '../vector'
|
||||
import { Dom } from './dom'
|
||||
import { Fragment } from '../vector/fragment/fragment'
|
||||
|
||||
@ -246,7 +246,7 @@ describe('Dom', () => {
|
||||
|
||||
describe('contains()', () => {
|
||||
it('should return `true` if the element is ancestor of the given element', () => {
|
||||
const svg = new Svg()
|
||||
const svg = new SVG()
|
||||
const group1 = svg.group().addClass('test')
|
||||
const group2 = group1.group()
|
||||
const rect = group2.rect()
|
||||
@ -262,7 +262,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should return `false` if the element is ancestor of the given element', () => {
|
||||
expect(new Svg().contains(new G())).toBeFalse()
|
||||
expect(new SVG().contains(new G())).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
@ -290,13 +290,13 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
describe('parent()', () => {
|
||||
let svg: Svg
|
||||
let svg: SVG
|
||||
let rect: Rect
|
||||
let group1: G
|
||||
let group2: G
|
||||
|
||||
beforeEach(() => {
|
||||
svg = new Svg().addTo(document.body)
|
||||
svg = new SVG().addTo(document.body)
|
||||
group1 = svg.group().addClass('test')
|
||||
group2 = group1.group()
|
||||
rect = group2.rect()
|
||||
@ -311,7 +311,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should return the closest parent with the correct type', () => {
|
||||
expect(rect.parent(Svg)).toBe(svg)
|
||||
expect(rect.parent(SVG)).toBe(svg)
|
||||
})
|
||||
|
||||
it('should return the closest parent matching the selector', () => {
|
||||
@ -319,7 +319,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should return `null` if the element do not have a parent', () => {
|
||||
expect(new Svg().parent()).toBe(null)
|
||||
expect(new SVG().parent()).toBe(null)
|
||||
})
|
||||
|
||||
it('should return `null` if it cannot find a parent matching the argument', () => {
|
||||
@ -328,14 +328,14 @@ describe('Dom', () => {
|
||||
|
||||
it('should return `null` if it cannot find a parent matching the argument in a #document-fragment', () => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
const svg = new Svg().addTo(fragment)
|
||||
const svg = new SVG().addTo(fragment)
|
||||
const rect = svg.rect()
|
||||
expect(rect.parent('.not-there')).toBe(null)
|
||||
})
|
||||
|
||||
it('should return Dom if parent is #document-fragment', () => {
|
||||
const fragment = document.createDocumentFragment()
|
||||
const svg = new Svg().addTo(fragment)
|
||||
const svg = new SVG().addTo(fragment)
|
||||
expect(svg.parent()).toBeInstanceOf(Dom)
|
||||
})
|
||||
|
||||
@ -347,7 +347,7 @@ describe('Dom', () => {
|
||||
describe('parents()', () => {
|
||||
let div1: Dom
|
||||
let div2: Dom
|
||||
let svg: Svg
|
||||
let svg: SVG
|
||||
let rect: Rect
|
||||
let group1: G
|
||||
let group2: G
|
||||
@ -355,7 +355,7 @@ describe('Dom', () => {
|
||||
beforeEach(() => {
|
||||
div1 = new Dom().appendTo(document.body)
|
||||
div2 = new Dom().appendTo(div1)
|
||||
svg = new Svg().appendTo(div2)
|
||||
svg = new SVG().appendTo(div2)
|
||||
group1 = svg.group().addClass('test')
|
||||
group2 = group1.group()
|
||||
rect = group2.rect()
|
||||
@ -374,7 +374,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should return all the ancestors until the specified type', () => {
|
||||
expect(rect.parents(Svg)).toEqual([group2, group1])
|
||||
expect(rect.parents(SVG)).toEqual([group2, group1])
|
||||
})
|
||||
|
||||
it('should return all the ancestors until the specified selector', () => {
|
||||
@ -452,7 +452,7 @@ describe('Dom', () => {
|
||||
const node = createSVGNode('svg')
|
||||
g.add(node)
|
||||
expect(g.children().length).toBe(1)
|
||||
expect(g.get(0)).toBeInstanceOf(Svg)
|
||||
expect(g.get(0)).toBeInstanceOf(SVG)
|
||||
})
|
||||
})
|
||||
|
||||
@ -938,11 +938,11 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
describe('wrap()', () => {
|
||||
let svg: Svg
|
||||
let svg: SVG
|
||||
let rect: Rect
|
||||
|
||||
beforeEach(function () {
|
||||
svg = new Svg()
|
||||
svg = new SVG()
|
||||
rect = svg.rect()
|
||||
})
|
||||
|
||||
@ -1032,7 +1032,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should replace the current element with the imported elements with `outerXML` is `true`', () => {
|
||||
const svg = new Svg()
|
||||
const svg = new SVG()
|
||||
const g = svg.group()
|
||||
g.xml('<rect /><circle />', true, namespaces.svg)
|
||||
expect(svg.children()[0]).toBeInstanceOf(Rect)
|
||||
@ -1040,7 +1040,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should return the parent when `outerXML` is `true`', () => {
|
||||
const svg = new Svg()
|
||||
const svg = new SVG()
|
||||
const g = svg.group()
|
||||
expect(g.xml('<rect /><circle />', true, namespaces.svg)).toBe(svg)
|
||||
expect(svg.children()[0]).toBeInstanceOf(Rect)
|
||||
@ -1048,7 +1048,7 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
it('should work without a parent', () => {
|
||||
const svg = new Svg()
|
||||
const svg = new SVG()
|
||||
expect(svg.xml('<rect /><circle />', undefined, namespaces.svg)).toBe(
|
||||
svg,
|
||||
)
|
||||
@ -1062,12 +1062,12 @@ describe('Dom', () => {
|
||||
})
|
||||
|
||||
describe('getter', () => {
|
||||
let svg: Svg
|
||||
let svg: SVG
|
||||
let group: G
|
||||
let rect: Rect
|
||||
|
||||
beforeEach(() => {
|
||||
svg = new Svg().removeNamespace()
|
||||
svg = new SVG().removeNamespace()
|
||||
group = svg.group()
|
||||
rect = group.rect(123.456, 234.567)
|
||||
})
|
||||
@ -1100,7 +1100,7 @@ describe('Dom', () => {
|
||||
expect(
|
||||
svg.xml((el) => {
|
||||
if (el instanceof Rect) return new Circle()
|
||||
if (el instanceof Svg) el.removeNamespace()
|
||||
if (el instanceof SVG) el.removeNamespace()
|
||||
}),
|
||||
).toBe('<svg><g><circle></circle></g></svg>')
|
||||
})
|
||||
@ -1110,7 +1110,7 @@ describe('Dom', () => {
|
||||
expect(svg.xml(() => false)).toBe('')
|
||||
expect(
|
||||
svg.xml((el) => {
|
||||
if (el instanceof Svg) {
|
||||
if (el instanceof SVG) {
|
||||
el.removeNamespace()
|
||||
} else {
|
||||
return false
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Dom } from '../dom'
|
||||
import { G } from '../../vector/g/g'
|
||||
import { Svg } from '../../vector/svg/svg'
|
||||
import { SVG } from '../../vector/svg/svg'
|
||||
|
||||
describe('Dom', () => {
|
||||
describe('constructor', () => {
|
||||
@ -39,7 +39,7 @@ describe('Dom', () => {
|
||||
expect(g.type.toLowerCase()).toEqual('g')
|
||||
expect(g.node).toBeInstanceOf(SVGGElement)
|
||||
|
||||
const svg = new Svg({ x: 10, y: 10 })
|
||||
const svg = new SVG({ x: 10, y: 10 })
|
||||
expect(svg.type.toLowerCase()).toEqual('svg')
|
||||
expect(svg.node).toBeInstanceOf(SVGSVGElement)
|
||||
expect(svg.attr('x')).toEqual(10)
|
||||
|
@ -1,6 +1,3 @@
|
||||
import './animation'
|
||||
|
||||
export * from './global/version'
|
||||
export * from './dom'
|
||||
export * from './vector'
|
||||
export * from './animating'
|
||||
|
@ -143,6 +143,22 @@ describe('Box', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('toJSON()', () => {
|
||||
it('should create an object representation of Box', () => {
|
||||
const obj1 = new Box().toJSON()
|
||||
expect(obj1.x).toBe(0)
|
||||
expect(obj1.y).toBe(0)
|
||||
expect(obj1.width).toBe(0)
|
||||
expect(obj1.height).toBe(0)
|
||||
|
||||
const obj2 = new Box(1, 2, 3, 4).toJSON()
|
||||
expect(obj2.x).toBe(1)
|
||||
expect(obj2.y).toBe(2)
|
||||
expect(obj2.width).toBe(3)
|
||||
expect(obj2.height).toBe(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toArray()', () => {
|
||||
it('should return an array representation of the box', () => {
|
||||
expect(new Box(1, 2, 3, 4).toArray()).toEqual([1, 2, 3, 4])
|
||||
|
@ -2,32 +2,32 @@ import { Point } from './point'
|
||||
import { Matrix } from './matrix'
|
||||
|
||||
export class Box implements Box.BoxLike {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
public x: number
|
||||
public y: number
|
||||
public width: number
|
||||
public height: number
|
||||
|
||||
get w() {
|
||||
public get w() {
|
||||
return this.width
|
||||
}
|
||||
|
||||
get h() {
|
||||
public get h() {
|
||||
return this.height
|
||||
}
|
||||
|
||||
get x2() {
|
||||
public get x2() {
|
||||
return this.x + this.width
|
||||
}
|
||||
|
||||
get y2() {
|
||||
public get y2() {
|
||||
return this.y + this.height
|
||||
}
|
||||
|
||||
get cx() {
|
||||
public get cx() {
|
||||
return this.x + this.width / 2
|
||||
}
|
||||
|
||||
get cy() {
|
||||
public get cy() {
|
||||
return this.y + this.height / 2
|
||||
}
|
||||
|
||||
@ -66,10 +66,6 @@ export class Box implements Box.BoxLike {
|
||||
return this
|
||||
}
|
||||
|
||||
isNull() {
|
||||
return Box.isNull(this)
|
||||
}
|
||||
|
||||
merge(box: Box.BoxLike) {
|
||||
const x = Math.min(this.x, box.x)
|
||||
const y = Math.min(this.y, box.y)
|
||||
@ -78,7 +74,15 @@ export class Box implements Box.BoxLike {
|
||||
return new Box(x, y, width, height)
|
||||
}
|
||||
|
||||
transform(matrix: Matrix | Matrix.Raw) {
|
||||
isNull() {
|
||||
return Box.isNull(this)
|
||||
}
|
||||
|
||||
transform(matrix: Matrix.Raw) {
|
||||
return this.clone().transformO(matrix)
|
||||
}
|
||||
|
||||
transformO(matrix: Matrix.Raw) {
|
||||
const m = matrix instanceof Matrix ? matrix : new Matrix(matrix)
|
||||
|
||||
let xMin = Number.POSITIVE_INFINITY
|
||||
@ -101,17 +105,30 @@ export class Box implements Box.BoxLike {
|
||||
yMax = Math.max(yMax, point.y)
|
||||
})
|
||||
|
||||
return new Box(xMin, yMin, xMax - xMin, yMax - yMin)
|
||||
this.x = xMin
|
||||
this.y = yMin
|
||||
this.width = xMax - xMin
|
||||
this.height = yMax - yMin
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.x} ${this.y} ${this.width} ${this.height}`
|
||||
clone() {
|
||||
return new Box(this)
|
||||
}
|
||||
|
||||
toJSON(): Box.BoxLike {
|
||||
return { x: this.x, y: this.y, width: this.width, height: this.height }
|
||||
}
|
||||
|
||||
toArray(): Box.BoxArray {
|
||||
return [this.x, this.y, this.width, this.height]
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.x} ${this.y} ${this.width} ${this.height}`
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.toArray()
|
||||
}
|
||||
@ -123,6 +140,8 @@ export namespace Box {
|
||||
height: number
|
||||
}
|
||||
|
||||
export type BoxArray = [number, number, number, number]
|
||||
|
||||
export interface BoxObject {
|
||||
x?: number
|
||||
y?: number
|
||||
@ -132,8 +151,6 @@ export namespace Box {
|
||||
height?: number
|
||||
}
|
||||
|
||||
export type BoxArray = [number, number, number, number]
|
||||
|
||||
export function isNull(box: BoxLike) {
|
||||
return !box.width && !box.height && !box.x && !box.y
|
||||
}
|
||||
|
@ -307,6 +307,42 @@ describe('Color', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('clone()', () => {
|
||||
it('should clone a color', () => {
|
||||
const color1 = new Color()
|
||||
const clone1 = color1.clone()
|
||||
expect(clone1).not.toBe(color1)
|
||||
expect(clone1.r).toBe(255)
|
||||
expect(clone1.g).toBe(255)
|
||||
expect(clone1.b).toBe(255)
|
||||
expect(clone1.a).toBe(1)
|
||||
|
||||
const color2 = new Color(4, 3, 2, 1)
|
||||
const clone2 = color2.clone()
|
||||
expect(clone2).not.toBe(color2)
|
||||
expect(clone2.r).toBe(4)
|
||||
expect(clone2.g).toBe(3)
|
||||
expect(clone2.b).toBe(2)
|
||||
expect(clone2.a).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toJSON()', () => {
|
||||
it('should create an object representation of Color', () => {
|
||||
const obj1 = new Color().toJSON()
|
||||
expect(obj1.r).toBe(255)
|
||||
expect(obj1.g).toBe(255)
|
||||
expect(obj1.b).toBe(255)
|
||||
expect(obj1.a).toBe(1)
|
||||
|
||||
const obj2 = new Color(4, 3, 2, 1).toJSON()
|
||||
expect(obj2.r).toBe(4)
|
||||
expect(obj2.g).toBe(3)
|
||||
expect(obj2.b).toBe(2)
|
||||
expect(obj2.a).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toArray()', () => {
|
||||
it('should convert color to rgba array', () => {
|
||||
expect(new Color().toArray()).toEqual([255, 255, 255, 1])
|
||||
|
@ -107,6 +107,14 @@ export class Color implements Color.RGBALike {
|
||||
return Color.makeGrey(Math.round((this.r + this.g + this.b) / 3), this.a)
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Color(this)
|
||||
}
|
||||
|
||||
toJSON(): Color.RGBALike {
|
||||
return { r: this.r, g: this.g, b: this.b, a: this.a }
|
||||
}
|
||||
|
||||
toArray(): Color.RGBA {
|
||||
return [this.r, this.g, this.b, this.a]
|
||||
}
|
||||
|
@ -87,10 +87,6 @@ export class Matrix implements Matrix.MatrixLike {
|
||||
this.f = source.f != null ? source.f : base.f
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Matrix(this)
|
||||
}
|
||||
|
||||
equals(other: Matrix.Raw) {
|
||||
if (other instanceof Matrix && other === this) {
|
||||
return true
|
||||
@ -425,6 +421,21 @@ export class Matrix implements Matrix.MatrixLike {
|
||||
return this
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Matrix(this)
|
||||
}
|
||||
|
||||
toJSON(): Matrix.MatrixLike {
|
||||
return {
|
||||
a: this.a,
|
||||
b: this.b,
|
||||
c: this.c,
|
||||
d: this.d,
|
||||
e: this.e,
|
||||
f: this.f,
|
||||
}
|
||||
}
|
||||
|
||||
toArray(): Matrix.MatrixArray {
|
||||
return [this.a, this.b, this.c, this.d, this.e, this.f]
|
||||
}
|
||||
@ -434,14 +445,7 @@ export class Matrix implements Matrix.MatrixLike {
|
||||
}
|
||||
|
||||
valueOf(): Matrix.MatrixLike {
|
||||
return {
|
||||
a: this.a,
|
||||
b: this.b,
|
||||
c: this.c,
|
||||
d: this.d,
|
||||
e: this.e,
|
||||
f: this.f,
|
||||
}
|
||||
return this.toJSON()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,14 @@ import { TypeArray } from './type-array'
|
||||
import { UnitNumber } from './unit-number'
|
||||
|
||||
export class PathArray extends TypeArray<Path.Segment> {
|
||||
parse(d: string | number[] | PathArray | Path.Segment[] = 'M0 0') {
|
||||
return d instanceof PathArray
|
||||
? d.toArray()
|
||||
: parse(
|
||||
Array.isArray(d) ? Array.prototype.concat.apply([], d).toString() : d,
|
||||
)
|
||||
}
|
||||
|
||||
bbox() {
|
||||
return withPathContext((path) => {
|
||||
path.setAttribute('d', this.toString())
|
||||
@ -96,14 +104,6 @@ export class PathArray extends TypeArray<Path.Segment> {
|
||||
return this
|
||||
}
|
||||
|
||||
parse(d: string | number[] | PathArray | Path.Segment[] = 'M0 0') {
|
||||
return d instanceof PathArray
|
||||
? d.toArray()
|
||||
: parse(
|
||||
Array.isArray(d) ? Array.prototype.concat.apply([], d).toString() : d,
|
||||
)
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return toString(this)
|
||||
}
|
||||
|
@ -4,6 +4,26 @@ import { TypeArray } from './type-array'
|
||||
import { UnitNumber } from './unit-number'
|
||||
|
||||
export class PointArray extends TypeArray<[number, number]> {
|
||||
parse(raw: string | [number, number][] | number[] = [0, 0]) {
|
||||
const array: number[] = Array.isArray(raw)
|
||||
? Array.prototype.concat.apply([], raw)
|
||||
: raw
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map((str) => Number.parseFloat(str))
|
||||
|
||||
if (array.length % 2 !== 0) {
|
||||
array.pop()
|
||||
}
|
||||
|
||||
const points: [number, number][] = []
|
||||
for (let i = 0, l = array.length; i < l; i += 2) {
|
||||
points.push([array[i], array[i + 1]])
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
bbox() {
|
||||
let maxX = Number.NEGATIVE_INFINITY
|
||||
let maxY = Number.NEGATIVE_INFINITY
|
||||
@ -34,26 +54,6 @@ export class PointArray extends TypeArray<[number, number]> {
|
||||
return this
|
||||
}
|
||||
|
||||
parse(raw: string | [number, number][] | number[] = [0, 0]) {
|
||||
const array: number[] = Array.isArray(raw)
|
||||
? Array.prototype.concat.apply([], raw)
|
||||
: raw
|
||||
.trim()
|
||||
.split(/[\s,]+/)
|
||||
.map((str) => Number.parseFloat(str))
|
||||
|
||||
if (array.length % 2 !== 0) {
|
||||
array.pop()
|
||||
}
|
||||
|
||||
const points: [number, number][] = []
|
||||
for (let i = 0, l = array.length; i < l; i += 2) {
|
||||
points.push([array[i], array[i + 1]])
|
||||
}
|
||||
|
||||
return points
|
||||
}
|
||||
|
||||
size(width: number | string, height: number | string) {
|
||||
const box = this.bbox()
|
||||
const w = UnitNumber.toNumber(width)
|
||||
|
@ -70,6 +70,13 @@ describe('Point', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('valueOf()', () => {
|
||||
it('should create an array representation of Point', () => {
|
||||
const p = new Point(1, 2)
|
||||
expect(p.valueOf()).toEqual([1, 2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('toJSON()', () => {
|
||||
it('should create an object representation of Point', () => {
|
||||
const obj1 = new Point().toJSON()
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Matrix } from './matrix'
|
||||
|
||||
export class Point implements Point.PointLike {
|
||||
x: number
|
||||
y: number
|
||||
public x: number
|
||||
public y: number
|
||||
|
||||
constructor()
|
||||
constructor(p: Point.PointLike)
|
||||
@ -32,6 +32,14 @@ export class Point implements Point.PointLike {
|
||||
return [this.x, this.y]
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${this.x} ${this.y}`
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.toArray()
|
||||
}
|
||||
|
||||
transform(matrix: Matrix.Raw) {
|
||||
return this.clone().transformO(matrix)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { UnitNumber } from './unit-number'
|
||||
|
||||
describe('SVGNumber', () => {
|
||||
describe('UnitNumber', () => {
|
||||
let number: UnitNumber
|
||||
|
||||
beforeEach(() => {
|
||||
@ -245,13 +245,29 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('convert()', () => {
|
||||
describe('unitize()', () => {
|
||||
it('should change the unit of the number', () => {
|
||||
const number = new UnitNumber('12px').convert('%')
|
||||
const number = new UnitNumber('12px').unitize('%')
|
||||
expect(number.toString()).toBe('1200%')
|
||||
})
|
||||
})
|
||||
|
||||
describe('clone()', () => {
|
||||
it('should clone an UnitNumber', () => {
|
||||
const num1 = new UnitNumber()
|
||||
const clone1 = num1.clone()
|
||||
expect(clone1).not.toBe(num1)
|
||||
expect(clone1.unit).toBe('')
|
||||
expect(clone1.value).toBe(0)
|
||||
|
||||
const num2 = new UnitNumber('1px')
|
||||
const clone2 = num2.clone()
|
||||
expect(clone2).not.toBe(num2)
|
||||
expect(clone2.unit).toBe('px')
|
||||
expect(clone2.value).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Static Methods', () => {
|
||||
describe('create()', () => {
|
||||
it('should create a SVGNumber with default values', () => {
|
||||
@ -353,8 +369,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should plus with a number and a string take unit into account', () => {
|
||||
expect(UnitNumber.plus(1, '2')).toBe('3')
|
||||
expect(UnitNumber.plus('1', 2)).toBe('3')
|
||||
expect(UnitNumber.plus(1, '2')).toBe(3)
|
||||
expect(UnitNumber.plus('1', 2)).toBe(3)
|
||||
|
||||
expect(UnitNumber.plus(1, '2%')).toBe('102%')
|
||||
expect(UnitNumber.plus('1%', 2)).toBe('200.999999%')
|
||||
@ -367,8 +383,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should plus with two strings take unit into account', () => {
|
||||
expect(UnitNumber.plus('1', '2')).toBe('3')
|
||||
expect(UnitNumber.plus('1', '2')).toBe('3')
|
||||
expect(UnitNumber.plus('1', '2')).toBe(3)
|
||||
expect(UnitNumber.plus('1', 2)).toBe(3)
|
||||
|
||||
expect(UnitNumber.plus('1', '2%')).toBe('102%')
|
||||
expect(UnitNumber.plus('1%', '2')).toBe('200.999999%')
|
||||
@ -390,8 +406,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should minus with a number and a string take unit into account', () => {
|
||||
expect(UnitNumber.minus(1, '2')).toBe('-1')
|
||||
expect(UnitNumber.minus('1', 2)).toBe('-1')
|
||||
expect(UnitNumber.minus(1, '2')).toBe(-1)
|
||||
expect(UnitNumber.minus('1', 2)).toBe(-1)
|
||||
|
||||
expect(UnitNumber.minus(1, '2%')).toBe('98%')
|
||||
expect(UnitNumber.minus('1%', 2)).toBe('-199%')
|
||||
@ -404,8 +420,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should minus with two strings take unit into account', () => {
|
||||
expect(UnitNumber.minus('1', '2')).toBe('-1')
|
||||
expect(UnitNumber.minus('1', '2')).toBe('-1')
|
||||
expect(UnitNumber.minus('1', '2')).toBe(-1)
|
||||
expect(UnitNumber.minus('1', 2)).toBe(-1)
|
||||
|
||||
expect(UnitNumber.minus('1', '2%')).toBe('98%')
|
||||
expect(UnitNumber.minus('1%', '2')).toBe('-199%')
|
||||
@ -427,8 +443,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should times with a number and a string take unit into account', () => {
|
||||
expect(UnitNumber.times(1, '2')).toBe('2')
|
||||
expect(UnitNumber.times('1', 2)).toBe('2')
|
||||
expect(UnitNumber.times(1, '2')).toBe(2)
|
||||
expect(UnitNumber.times('1', 2)).toBe(2)
|
||||
|
||||
expect(UnitNumber.times(1, '2%')).toBe('2%')
|
||||
expect(UnitNumber.times('1%', 2)).toBe('2%')
|
||||
@ -441,8 +457,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should times with two strings take unit into account', () => {
|
||||
expect(UnitNumber.times('1', '2')).toBe('2')
|
||||
expect(UnitNumber.times('1', '2')).toBe('2')
|
||||
expect(UnitNumber.times('1', '2')).toBe(2)
|
||||
expect(UnitNumber.times('1', 2)).toBe(2)
|
||||
|
||||
expect(UnitNumber.times('1', '2%')).toBe('2%')
|
||||
expect(UnitNumber.times('1%', '2')).toBe('2%')
|
||||
@ -464,8 +480,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should divide with a number and a string take unit into account', () => {
|
||||
expect(UnitNumber.divide(1, '2')).toBe('0.5')
|
||||
expect(UnitNumber.divide('1', 2)).toBe('0.5')
|
||||
expect(UnitNumber.divide(1, '2')).toBe(0.5)
|
||||
expect(UnitNumber.divide('1', 2)).toBe(0.5)
|
||||
|
||||
expect(UnitNumber.divide(1, '2%')).toBe('5000%')
|
||||
expect(UnitNumber.divide('1%', 2)).toBe('0.5%')
|
||||
@ -478,8 +494,8 @@ describe('SVGNumber', () => {
|
||||
})
|
||||
|
||||
it('should divide with two strings take unit into account', () => {
|
||||
expect(UnitNumber.divide('1', '2')).toBe('0.5')
|
||||
expect(UnitNumber.divide('1', '2')).toBe('0.5')
|
||||
expect(UnitNumber.divide('1', '2')).toBe(0.5)
|
||||
expect(UnitNumber.divide('1', 2)).toBe(0.5)
|
||||
|
||||
expect(UnitNumber.divide('1', '2%')).toBe('5000%')
|
||||
expect(UnitNumber.divide('1%', '2')).toBe('0.5%')
|
||||
|
@ -51,18 +51,26 @@ export class UnitNumber implements UnitNumber.UnitNumberLike {
|
||||
return new UnitNumber(this.value / input.value, this.unit || input.unit)
|
||||
}
|
||||
|
||||
convert(unit: string) {
|
||||
unitize(unit: string) {
|
||||
return new UnitNumber(this.value, unit)
|
||||
}
|
||||
|
||||
toArray(): UnitNumber.UnitNumberArray {
|
||||
return [this.value, this.unit]
|
||||
naturalize() {
|
||||
return this.unit ? this.toString() : this.valueOf()
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new UnitNumber(this)
|
||||
}
|
||||
|
||||
toJSON(): UnitNumber.UnitNumberLike {
|
||||
return { value: this.value, unit: this.unit }
|
||||
}
|
||||
|
||||
toArray(): UnitNumber.UnitNumberArray {
|
||||
return [this.value, this.unit]
|
||||
}
|
||||
|
||||
toString() {
|
||||
const value =
|
||||
this.unit === '%'
|
||||
@ -108,28 +116,28 @@ export namespace UnitNumber {
|
||||
if (typeof left === 'number' && typeof right === 'number') {
|
||||
return left + right
|
||||
}
|
||||
return create(left).plus(right).toString()
|
||||
return create(left).plus(right).naturalize()
|
||||
}
|
||||
|
||||
export function minus(left: Raw, right: Raw) {
|
||||
if (typeof left === 'number' && typeof right === 'number') {
|
||||
return left - right
|
||||
}
|
||||
return create(left).minus(right).toString()
|
||||
return create(left).minus(right).naturalize()
|
||||
}
|
||||
|
||||
export function times(left: Raw, right: Raw) {
|
||||
if (typeof left === 'number' && typeof right === 'number') {
|
||||
return left * right
|
||||
}
|
||||
return create(left).times(right).toString()
|
||||
return create(left).times(right).naturalize()
|
||||
}
|
||||
|
||||
export function divide(left: Raw, right: Raw) {
|
||||
if (typeof left === 'number' && typeof right === 'number') {
|
||||
return left / right
|
||||
}
|
||||
return create(left).divide(right).toString()
|
||||
return create(left).divide(right).naturalize()
|
||||
}
|
||||
|
||||
export function normalize(v: number) {
|
||||
@ -142,11 +150,10 @@ export namespace UnitNumber {
|
||||
: v
|
||||
}
|
||||
|
||||
export const REGEX_NUMBER_UNIT =
|
||||
/^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i
|
||||
export const REGEX = /^([+-]?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?)([a-z%]*)$/i
|
||||
|
||||
export function parse(str: string): UnitNumberLike | null {
|
||||
const matches = str.match(REGEX_NUMBER_UNIT)
|
||||
const matches = str.match(REGEX)
|
||||
if (matches) {
|
||||
let value = normalize(Number.parseFloat(matches[1]))
|
||||
const unit = matches[5] || ''
|
||||
|
@ -11,7 +11,7 @@ import { RadialGradient } from '../vector/gradient/radial'
|
||||
import { Marker } from '../vector/marker/marker'
|
||||
import { Mask } from '../vector/mask/mask'
|
||||
import { Pattern } from '../vector/pattern/pattern'
|
||||
import { Svg } from '../vector/svg/svg'
|
||||
import { SVG } from '../vector/svg/svg'
|
||||
import { Symbol } from '../vector/symbol/symbol'
|
||||
import { Dom } from '../dom/dom'
|
||||
import { Vector } from '../vector/vector/vector'
|
||||
@ -44,7 +44,7 @@ export type ElementMap<T> =
|
||||
T extends SVGMarkerElement ? Marker :
|
||||
T extends SVGMaskElement ? Mask :
|
||||
T extends SVGPatternElement ? Pattern :
|
||||
T extends SVGSVGElement ? Svg :
|
||||
T extends SVGSVGElement ? SVG :
|
||||
T extends SVGSymbolElement ? Symbol : // eslint-disable-line
|
||||
|
||||
T extends SVGCircleElement ? Circle :
|
||||
|
@ -0,0 +1,29 @@
|
||||
import { G } from '../g/g'
|
||||
import { Circle } from '../circle/circle'
|
||||
import { AnimateMotion } from './animate-motion'
|
||||
|
||||
describe('Animate', () => {
|
||||
describe('constructor()', () => {
|
||||
it('should create an AnimateMotion', () => {
|
||||
expect(AnimateMotion.create()).toBeInstanceOf(AnimateMotion)
|
||||
})
|
||||
|
||||
it('should create an AnimateMotion with given attributes', () => {
|
||||
expect(AnimateMotion.create({ id: 'foo' }).id()).toBe('foo')
|
||||
})
|
||||
|
||||
it('should create an AnimateMotion in a group', () => {
|
||||
const group = new G()
|
||||
const animate = group.animateMotion({ id: 'foo' })
|
||||
expect(animate.attr('id')).toBe('foo')
|
||||
expect(animate).toBeInstanceOf(AnimateMotion)
|
||||
})
|
||||
|
||||
it('should create an AnimateMotion in a circle', () => {
|
||||
const circle = new Circle()
|
||||
const animate = circle.animateMotion({ id: 'foo' })
|
||||
expect(animate.attr('id')).toBe('foo')
|
||||
expect(animate).toBeInstanceOf(AnimateMotion)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,13 @@
|
||||
import { Animation } from '../animate/animation'
|
||||
import { SVGAnimateMotionAttributes } from './types'
|
||||
|
||||
@AnimateMotion.register('AnimateMotion')
|
||||
export class AnimateMotion extends Animation<SVGAnimateMotionElement> {}
|
||||
|
||||
export namespace AnimateMotion {
|
||||
export function create<Attributes extends SVGAnimateMotionAttributes>(
|
||||
attrs?: Attributes | null,
|
||||
) {
|
||||
return new AnimateMotion(attrs)
|
||||
}
|
||||
}
|
13
packages/x6-vector/src/vector/animate-motion/exts.ts
Normal file
13
packages/x6-vector/src/vector/animate-motion/exts.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Base } from '../common/base'
|
||||
import { AnimateMotion } from './animate-motion'
|
||||
import { SVGAnimateMotionAttributes } from './types'
|
||||
|
||||
export class ContainerExtension<
|
||||
TSVGElement extends SVGElement,
|
||||
> extends Base<TSVGElement> {
|
||||
animateMotion<Attributes extends SVGAnimateMotionAttributes>(
|
||||
attrs?: Attributes | null,
|
||||
) {
|
||||
return AnimateMotion.create(attrs).appendTo(this)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { G } from '../g/g'
|
||||
import { Circle } from '../circle/circle'
|
||||
import { AnimateTransform } from './animate-transform'
|
||||
|
||||
describe('Animate', () => {
|
||||
describe('constructor()', () => {
|
||||
it('should create an AnimateTransform', () => {
|
||||
expect(AnimateTransform.create()).toBeInstanceOf(AnimateTransform)
|
||||
})
|
||||
|
||||
it('should create an AnimateTransform with given attributes', () => {
|
||||
expect(AnimateTransform.create({ id: 'foo' }).id()).toBe('foo')
|
||||
})
|
||||
|
||||
it('should create an AnimateTransform in a group', () => {
|
||||
const group = new G()
|
||||
const animate = group.animateTransform({ id: 'foo' })
|
||||
expect(animate.attr('id')).toBe('foo')
|
||||
expect(animate).toBeInstanceOf(AnimateTransform)
|
||||
})
|
||||
|
||||
it('should create an AnimateTransform in a circle', () => {
|
||||
const circle = new Circle()
|
||||
const animate = circle.animateTransform({ id: 'foo' })
|
||||
expect(animate.attr('id')).toBe('foo')
|
||||
expect(animate).toBeInstanceOf(AnimateTransform)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,13 @@
|
||||
import { Animation } from '../animate/animation'
|
||||
import { SVGAnimateTransformAttributes } from './types'
|
||||
|
||||
@AnimateTransform.register('AnimateTransform')
|
||||
export class AnimateTransform extends Animation<SVGAnimateTransformElement> {}
|
||||
|
||||
export namespace AnimateTransform {
|
||||
export function create<Attributes extends SVGAnimateTransformAttributes>(
|
||||
attrs?: Attributes | null,
|
||||
) {
|
||||
return new AnimateTransform(attrs)
|
||||
}
|
||||
}
|
13
packages/x6-vector/src/vector/animate-transform/exts.ts
Normal file
13
packages/x6-vector/src/vector/animate-transform/exts.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Base } from '../common/base'
|
||||
import { AnimateTransform } from './animate-transform'
|
||||
import { SVGAnimateTransformAttributes } from './types'
|
||||
|
||||
export class ContainerExtension<
|
||||
TSVGElement extends SVGElement,
|
||||
> extends Base<TSVGElement> {
|
||||
animateTransform<Attributes extends SVGAnimateTransformAttributes>(
|
||||
attrs?: Attributes | null,
|
||||
) {
|
||||
return AnimateTransform.create(attrs).appendTo(this)
|
||||
}
|
||||
}
|
98
packages/x6-vector/src/vector/animate/animate.test.ts
Normal file
98
packages/x6-vector/src/vector/animate/animate.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { G } from '../g/g'
|
||||
import { Circle } from '../circle/circle'
|
||||
import { Animate } from './animate'
|
||||
|
||||
describe('Animate', () => {
|
||||
describe('constructor()', () => {
|
||||
it('should create an Animate', () => {
|
||||
expect(Animate.create()).toBeInstanceOf(Animate)
|
||||
})
|
||||
|
||||
it('should create an Animate with given attributes', () => {
|
||||
expect(Animate.create({ id: 'foo' }).id()).toBe('foo')
|
||||
})
|
||||
|
||||
it('should create an Animate in a group', () => {
|
||||
const group = new G()
|
||||
const animate = group.animate({ id: 'foo' })
|
||||
expect(animate.attr('id')).toBe('foo')
|
||||
expect(animate).toBeInstanceOf(Animate)
|
||||
})
|
||||
|
||||
it('should create an Animate in a circle', () => {
|
||||
const circle = new Circle()
|
||||
const animate = circle.animate({ id: 'foo' })
|
||||
expect(animate.attr('id')).toBe('foo')
|
||||
expect(animate).toBeInstanceOf(Animate)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sugar methods with attributes', () => {
|
||||
const methods = [
|
||||
'from',
|
||||
'to',
|
||||
'by',
|
||||
'calcMode',
|
||||
'values',
|
||||
'keyTimes',
|
||||
'keySplines',
|
||||
'begin',
|
||||
'end',
|
||||
'dur',
|
||||
'min',
|
||||
'max',
|
||||
'repeatCount',
|
||||
'repeatDur',
|
||||
'restartMode',
|
||||
'fillMode',
|
||||
'additive',
|
||||
'accumulate',
|
||||
'attributeName',
|
||||
'attributeType',
|
||||
]
|
||||
const values = [
|
||||
10,
|
||||
20,
|
||||
30,
|
||||
'linear',
|
||||
'0 1 2 3 4',
|
||||
'foo',
|
||||
'bar',
|
||||
'1s',
|
||||
'2s',
|
||||
'3s',
|
||||
'0s',
|
||||
'100s',
|
||||
3,
|
||||
3000,
|
||||
'whenNotActive',
|
||||
'freeze',
|
||||
'replace',
|
||||
'none',
|
||||
'color',
|
||||
'CSS',
|
||||
]
|
||||
|
||||
methods.forEach((method, index) => {
|
||||
const val = values[index] as any
|
||||
const attrMap = {
|
||||
restartMode: 'restart',
|
||||
fillMode: 'fill',
|
||||
}
|
||||
const attr = attrMap[method as keyof typeof attrMap] || method
|
||||
|
||||
it(`should call attribute with "${method}" and return itself`, () => {
|
||||
const animate = new Animate()
|
||||
const spy = spyOn(animate, 'attr').and.callThrough()
|
||||
expect(animate[method as 'from'](val)).toBe(animate)
|
||||
expect(spy).toHaveBeenCalledWith(attr, val)
|
||||
})
|
||||
|
||||
it(`should get the "${method}" atribute`, () => {
|
||||
const animate = Animate.create()
|
||||
animate[method as 'from'](val)
|
||||
expect(animate[method as 'from']()).toBe(val)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
13
packages/x6-vector/src/vector/animate/animate.ts
Normal file
13
packages/x6-vector/src/vector/animate/animate.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Animation } from './animation'
|
||||
import { SVGAnimateAttributes } from './types'
|
||||
|
||||
@Animate.register('Animate')
|
||||
export class Animate extends Animation<SVGAnimateElement> {}
|
||||
|
||||
export namespace Animate {
|
||||
export function create<Attributes extends SVGAnimateAttributes>(
|
||||
attrs?: Attributes | null,
|
||||
) {
|
||||
return new Animate(attrs)
|
||||
}
|
||||
}
|
156
packages/x6-vector/src/vector/animate/animation.ts
Normal file
156
packages/x6-vector/src/vector/animate/animation.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { Base } from '../common/base'
|
||||
import {
|
||||
SVGAnimationValueCalcMode,
|
||||
SVGAnimationTimingRestartMode,
|
||||
SVGAnimationTimingRepeatCount,
|
||||
SVGAnimationTimingRepeatDuration,
|
||||
SVGAnimationTimingFillMode,
|
||||
AnimationAttributeTargetAttributeType,
|
||||
SVGAnimationAdditionAttributeAdditive,
|
||||
SVGAnimationAdditionAttributeAccumulate,
|
||||
} from '../types'
|
||||
|
||||
export class Animation<
|
||||
TSVGAnimationElement extends
|
||||
| SVGAnimateElement
|
||||
| SVGAnimateMotionElement
|
||||
| SVGAnimateTransformElement,
|
||||
> extends Base<TSVGAnimationElement> {
|
||||
// #region SVGAnimationValueAttributes
|
||||
|
||||
from(): string | number
|
||||
from(v: string | number | null): this
|
||||
from(v?: string | number | null) {
|
||||
return this.attr('from', v)
|
||||
}
|
||||
|
||||
to(): string | number
|
||||
to(v: string | number | null): this
|
||||
to(v?: string | number | null) {
|
||||
return this.attr('to', v)
|
||||
}
|
||||
|
||||
by(): string | number
|
||||
by(v: string | number | null): this
|
||||
by(v?: string | number | null) {
|
||||
return this.attr('by', v)
|
||||
}
|
||||
|
||||
calcMode(): SVGAnimationValueCalcMode
|
||||
calcMode(mode: SVGAnimationValueCalcMode | null): this
|
||||
calcMode(mode?: SVGAnimationValueCalcMode | null) {
|
||||
return this.attr('calcMode', mode)
|
||||
}
|
||||
|
||||
values(): string
|
||||
values(v: string | null): this
|
||||
values(v?: string | null) {
|
||||
return this.attr('values', v)
|
||||
}
|
||||
|
||||
keyTimes(): string
|
||||
keyTimes(v: string | null): this
|
||||
keyTimes(v?: string | null) {
|
||||
return this.attr('keyTimes', v)
|
||||
}
|
||||
|
||||
keySplines(): string
|
||||
keySplines(v: string | null): this
|
||||
keySplines(v?: string | null) {
|
||||
return this.attr('keySplines', v)
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region SVGAnimationTimingAttributes
|
||||
|
||||
begin(): string
|
||||
begin(v: string | null): this
|
||||
begin(v?: string | null) {
|
||||
return this.attr('begin', v)
|
||||
}
|
||||
|
||||
end(): string
|
||||
end(v: string | null): this
|
||||
end(v?: string | null) {
|
||||
return this.attr('end', v)
|
||||
}
|
||||
|
||||
dur(): string
|
||||
dur(v: string | null): this
|
||||
dur(v?: string | null) {
|
||||
return this.attr('dur', v)
|
||||
}
|
||||
|
||||
min(): string
|
||||
min(v: string | null): this
|
||||
min(v?: string | null) {
|
||||
return this.attr('min', v)
|
||||
}
|
||||
|
||||
max(): string
|
||||
max(v: string | null): this
|
||||
max(v?: string | null) {
|
||||
return this.attr('max', v)
|
||||
}
|
||||
|
||||
repeatCount(): SVGAnimationTimingRepeatCount
|
||||
repeatCount(v: SVGAnimationTimingRepeatCount | null): this
|
||||
repeatCount(v?: SVGAnimationTimingRepeatCount | null) {
|
||||
return this.attr('repeatCount', v)
|
||||
}
|
||||
|
||||
repeatDur(): SVGAnimationTimingRepeatDuration
|
||||
repeatDur(v: SVGAnimationTimingRepeatDuration | null): this
|
||||
repeatDur(v?: SVGAnimationTimingRepeatDuration | null) {
|
||||
return this.attr('repeatDur', v)
|
||||
}
|
||||
|
||||
restartMode(): SVGAnimationTimingRestartMode
|
||||
restartMode(v: SVGAnimationTimingRestartMode | null): this
|
||||
restartMode(v?: SVGAnimationTimingRestartMode | null) {
|
||||
return this.attr('restart', v)
|
||||
}
|
||||
|
||||
fillMode(): SVGAnimationTimingFillMode
|
||||
fillMode(v: SVGAnimationTimingFillMode | null): this
|
||||
fillMode(v?: SVGAnimationTimingFillMode | null) {
|
||||
return this.attr('fill', v)
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region SVGAnimationAdditionAttributes
|
||||
|
||||
additive(): SVGAnimationAdditionAttributeAdditive
|
||||
additive(v: SVGAnimationAdditionAttributeAdditive | null): this
|
||||
additive(v?: SVGAnimationAdditionAttributeAdditive | null) {
|
||||
return this.attr('additive', v)
|
||||
}
|
||||
|
||||
accumulate(): SVGAnimationAdditionAttributeAccumulate
|
||||
accumulate(v: SVGAnimationAdditionAttributeAccumulate | null): this
|
||||
accumulate(v?: SVGAnimationAdditionAttributeAccumulate | null) {
|
||||
return this.attr('accumulate', v)
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region AnimationAttributeTargetAttributes
|
||||
|
||||
attributeName(): string
|
||||
attributeName(name: string | null): this
|
||||
attributeName(name?: string | null) {
|
||||
return this.attr('attributeName', name)
|
||||
}
|
||||
|
||||
attributeType(): AnimationAttributeTargetAttributeType
|
||||
attributeType(type: AnimationAttributeTargetAttributeType | null): this
|
||||
attributeType(type?: AnimationAttributeTargetAttributeType | null) {
|
||||
return this.attr('attributeType', type)
|
||||
}
|
||||
|
||||
// #endregion
|
||||
}
|
||||
|
||||
export namespace Animation {}
|
11
packages/x6-vector/src/vector/animate/exts.ts
Normal file
11
packages/x6-vector/src/vector/animate/exts.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Base } from '../common/base'
|
||||
import { Animate } from './animate'
|
||||
import { SVGAnimateAttributes } from './types'
|
||||
|
||||
export class ContainerExtension<
|
||||
TSVGElement extends SVGElement,
|
||||
> extends Base<TSVGElement> {
|
||||
animate<Attributes extends SVGAnimateAttributes>(attrs?: Attributes | null) {
|
||||
return Animate.create(attrs).appendTo(this)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user