fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
167 lines (146 loc) • 4.41 kB
text/typescript
import { noop } from '../../constants';
import { requestAnimFrame } from './AnimationFrameProvider';
import { runningAnimations } from './AnimationRegistry';
import { defaultEasing } from './easing';
import type {
AnimationState,
TAbortCallback,
TBaseAnimationOptions,
TEasingFunction,
TOnAnimationChangeCallback,
} from './types';
const defaultAbort = () => false;
export abstract class AnimationBase<
T extends number | number[] = number | number[],
> {
declare readonly startValue: T;
declare readonly endValue: T;
declare readonly duration: number;
declare readonly delay: number;
protected declare readonly byValue: T;
protected declare readonly easing: TEasingFunction<T>;
private declare readonly _onStart: VoidFunction;
private declare readonly _onChange: TOnAnimationChangeCallback<T, void>;
private declare readonly _onComplete: TOnAnimationChangeCallback<T, void>;
private declare readonly _abort: TAbortCallback<T>;
/**
* Used to register the animation to a target object
* so that it can be cancelled within the object context
*/
declare readonly target?: unknown;
private _state: AnimationState = 'pending';
/**
* Time %, or the ratio of `timeElapsed / duration`
* @see tick
*/
durationProgress = 0;
/**
* Value %, or the ratio of `(currentValue - startValue) / (endValue - startValue)`
*/
valueProgress = 0;
/**
* Current value
*/
declare value: T;
/**
* Animation start time ms
*/
private declare startTime: number;
constructor({
startValue,
byValue,
duration = 500,
delay = 0,
easing = defaultEasing,
onStart = noop,
onChange = noop,
onComplete = noop,
abort = defaultAbort,
target,
}: TBaseAnimationOptions<T>) {
this.tick = this.tick.bind(this);
this.duration = duration;
this.delay = delay;
this.easing = easing;
this._onStart = onStart;
this._onChange = onChange;
this._onComplete = onComplete;
this._abort = abort;
this.target = target;
this.startValue = startValue;
this.byValue = byValue;
this.value = this.startValue;
this.endValue = Object.freeze(this.calculate(this.duration).value);
}
get state() {
return this._state;
}
isDone() {
return this._state === 'aborted' || this._state === 'completed';
}
/**
* Calculate the current value based on the easing parameters
* @param timeElapsed in ms
* @protected
*/
protected abstract calculate(timeElapsed: number): {
value: T;
valueProgress: number;
};
start() {
const firstTick: FrameRequestCallback = (timestamp) => {
if (this._state !== 'pending') return;
this.startTime = timestamp || +new Date();
this._state = 'running';
this._onStart();
this.tick(this.startTime);
};
this.register();
// setTimeout(cb, 0) will run cb on the next frame, causing a delay
// we don't want that
if (this.delay > 0) {
setTimeout(() => requestAnimFrame(firstTick), this.delay);
} else {
requestAnimFrame(firstTick);
}
}
private tick(t: number) {
const durationMs = (t || +new Date()) - this.startTime;
const boundDurationMs = Math.min(durationMs, this.duration);
this.durationProgress = boundDurationMs / this.duration;
const { value, valueProgress } = this.calculate(boundDurationMs);
this.value = Object.freeze(value);
this.valueProgress = valueProgress;
if (this._state === 'aborted') {
return;
} else if (
this._abort(this.value, this.valueProgress, this.durationProgress)
) {
this._state = 'aborted';
this.unregister();
} else if (durationMs >= this.duration) {
this.durationProgress = this.valueProgress = 1;
this._onChange(this.endValue, this.valueProgress, this.durationProgress);
this._state = 'completed';
this._onComplete(
this.endValue,
this.valueProgress,
this.durationProgress,
);
this.unregister();
} else {
this._onChange(this.value, this.valueProgress, this.durationProgress);
requestAnimFrame(this.tick);
}
}
private register() {
runningAnimations.push(this as unknown as AnimationBase);
}
private unregister() {
runningAnimations.remove(this as unknown as AnimationBase);
}
abort() {
this._state = 'aborted';
this.unregister();
}
}