animatable-js
Version:
This package allows easy and light implementation of linear or curved animation in javascript. (Especially suitable in a development environment on web components or canvas.)
171 lines (140 loc) • 5.58 kB
text/typescript
import { Ticker } from "./ticker";
import { AnimationStatus } from "./animatable";
import { AnimationListenable } from "./animation_listenable";
/**
* This class implements the fundamental features that make up the animation.
*
* See Also, this class provides the essential groundwork for animation functionality.
* It serves as a foundational component that simplifies the implementation process.
* As a result, developers can more easily create various complex animation features.
*/
export class AnimationController extends AnimationListenable {
/** This tween is mainly used to calculate the progress value. */
private tween: {begin: number, end: number};
/** An activated ticker about this animation controller. */
private activeTicker?: Ticker;
private _status: AnimationStatus = AnimationStatus.NONE;
get status() { return this._status; }
set status(newStatus: AnimationStatus) {
if (this._status != newStatus) {
this.notifyStatusListeners(this._status = newStatus);
}
}
private _value: number;
get value() { return this._value };
set value(newValue: number) {
if (this._value != newValue) {
this.notifyListeners(this._value = newValue);
}
}
constructor(
public duration: number,
public lowerValue: number = 0,
public upperValue: number = 1,
initialValue: number = lowerValue,
) {
super();
console.assert(duration != null, "An animation duration cannot be null.")
console.assert(duration != 0, "An animation duration cannot be 0.");
if (this.lowerValue > this.upperValue) {
throw new Error("The lowerValue must be less than the upperValue.");
}
this._value = initialValue;
}
/** Returns a relative range of animation value. */
get range(): number {
return this.upperValue - this.lowerValue;
}
/** Returns a relative value of aniomatin from 0 to 1. */
get relValue(): number {
const vector = this.value - this.lowerValue;
return vector / this.range;
}
/**
* Returns the relative value regardless of the progress direction
* of the animation value from 0 to 1.
*/
get progressValue(): number {
const begin = this.tween.begin;
const end = this.tween.end;
const relVector = end - begin;
return (this.relValue - begin) / relVector;
}
forward(duration?: number) {
this.animateTo(this.upperValue, duration);
}
backward(duration?: number) {
this.animateTo(this.lowerValue, duration);
}
repeat() {
this.addStatusListener(status => {
if (status == AnimationStatus.FORWARDED) this.backward();
if (status == AnimationStatus.BACKWARDED) this.forward();
});
if (this.status == AnimationStatus.NONE
|| this.status == AnimationStatus.BACKWARDED) {
this.forward();
}
}
animateTo(value: number, duration?: number) {
this.animate(this.value, value, duration);
}
animate(
from: number,
to: number,
duration: number = this.duration
) {
if (Math.abs(from - to) < 1e-10) return; // delta < precision error tolerance
console.assert(from >= this.lowerValue, "A given [from] is less than the min-range.");
console.assert(to <= this.upperValue, "A given [to] is larger than the max-range.");
// Sets initial related animation values.
this.value = from;
this.tween = {begin: from, end: to};
// Whether a value should be increased.
const isForward = to > from;
// Update the status before the animation starts.
this.status = isForward
? AnimationStatus.FORWARD
: AnimationStatus.BACKWARD;
// A total move distance of start to end.
const rDistance = this.range;
const rDuration = duration / rDistance;
this.activeTicker?.dispose();
this.activeTicker = new Ticker(elapsedDelta => {
const delta = elapsedDelta / rDuration;
const available = isForward ? delta : -delta;
const consumed = this.consume(from, to, available);
if (Math.abs(available - consumed) > 1e-10) { // unconsumed > precision error tolerance
this.value = to;
this.dispose();
// Update the status after the animation ends.
this.status = isForward
? AnimationStatus.FORWARDED
: AnimationStatus.BACKWARDED;
return;
}
// A value should not be overflowed by consumed value.
this.value += consumed;
});
}
private consume(
from: number,
to: number,
available: number
) {
const absValue = this.value + available;
const relValue = to - absValue;
return to > from // is forward
? relValue <= 0 ? relValue : available
: relValue >= 0 ? relValue : available;
}
dispose(): void {
this.activeTicker?.dispose();
this.activeTicker = null;
}
reset() {
this.status = AnimationStatus.NONE;
this.value = this.lowerValue;
this.tween = null;
}
}