animare
Version:
Advanced animation library for modern JavaScript.
261 lines (198 loc) • 9.29 kB
text/typescript
import { Direction, Timing } from './types.js';
import { isAlternateDirection, isReverseDirection, validateAnimationValues } from './utils/helpers.js';
import { clamp, normalizePercentage } from './utils/utils.js';
import type { AnimationInfo, AnimationPreparedOptions } from './types.js';
export default class Animation {
/** The index of the animation. */
#index: number;
/** The start time of the animation, without accounting for delays, in milliseconds. */
#start: number;
/** The end time of the animation, without accounting for delays and repeats, in milliseconds. */
#end: number;
/** The starting point in time, in milliseconds. */
#startPoint: number;
/** The end point in time, including delays and repeats, in milliseconds. */
endPoint: number;
/** The starting value of the animation, relative to its direction. */
#startValue: number;
/** The current animated value. */
#value: number;
/** The ending value of the animation, relative to its direction. */
#endValue: number;
/** The number of remaining times to apply the delay. */
#delayCount: number;
/** The current lap of the alternate direction. */
#alternateLap: 0 | 1 = 0;
/** The number of remaining times the animation should play. */
#playCount: number;
/** The current animation progress, from `0` to `1`, excluding delays. */
#progress: number = 0;
/** The overall progress, from `0` to `1`, including delays and repeats. */
#overallProgress: number = 0;
/** The elapsed time of the animation, in milliseconds. */
#elapsedTime: number = 0;
/** Indicates whether the animation has finished. */
#isFinished: boolean = false;
/** Indicates whether the animation is currently playing. */
#isPlaying: boolean = false;
/** Indicates whether the animation is in reverse. */
#isReverse: boolean;
/** Indicates whether the animation alternates direction. */
#isAlternate: boolean;
/** The reference to the prepared animation values. */
animationRef: AnimationPreparedOptions;
#previousTimelineRef: Animation | undefined;
#isProgressAt(progress: number, tolerance = 0.001): boolean {
return Math.abs(this.#progress - progress) < tolerance;
}
#isTimeAt(time: number, tolerance = 5): boolean {
return Math.abs(this.#elapsedTime - time) < tolerance;
}
// preserve info object reference
#infoRef: AnimationInfo = Object.create(null);
get info(): AnimationInfo {
return Object.assign(this.#infoRef, {
name: this.animationRef.name,
index: this.#index,
value: this.#value,
progress: this.#progress,
overallProgress: this.#overallProgress,
elapsedTime: this.#elapsedTime,
isFinished: this.#isFinished,
delayCount: this.#delayCount,
playCount: this.#playCount,
isPlaying: this.#isPlaying,
isProgressAt: this.#isProgressAt.bind(this),
isTimeAt: this.#isTimeAt.bind(this),
});
}
constructor(animation: AnimationPreparedOptions, previousTimeline: Animation | undefined, index: number) {
this.#index = index;
this.animationRef = animation;
this.#previousTimelineRef = previousTimeline;
this.Setup();
}
public Setup() {
this.#isFinished = false;
this.#isPlaying = false;
this.#elapsedTime = 0;
this.#progress = 0;
this.#overallProgress = 0;
this.#alternateLap = 0;
this.#playCount = 0;
this.#delayCount = 0;
this.#isAlternate = isAlternateDirection(this.animationRef.direction);
this.#isReverse = isReverseDirection(this.animationRef.direction);
this.#startValue = this.#isReverse ? this.animationRef.to : this.animationRef.from;
this.#value = this.#isReverse ? this.animationRef.to : this.animationRef.from;
this.#endValue = this.#isReverse ? this.animationRef.from : this.animationRef.to;
const offset = this.animationRef.offset;
const delay = this.animationRef.delayCount === 0 ? 0 : this.animationRef.delay;
const overallDuration = this.animationRef.duration * this.animationRef.playCount + delay * this.animationRef.delayCount;
// ensure that the first animation is always `Timing.FromStart`
const timing = this.#index === 0 ? Timing.FromStart : this.animationRef.timing;
switch (timing) {
case Timing.FromStart:
this.#startPoint = offset;
break;
case Timing.AfterPrevious:
if (!this.#previousTimelineRef) throw new Error('The previous animation is not defined.');
this.#startPoint = this.#previousTimelineRef.endPoint + offset;
break;
case Timing.WithPrevious:
if (!this.#previousTimelineRef) throw new Error('The previous animation is not defined.');
this.#startPoint = this.#previousTimelineRef.#startPoint + offset;
break;
}
this.endPoint = this.#startPoint + overallDuration;
this.#start = this.#startPoint + delay;
this.#end = this.#start + this.animationRef.duration;
}
public Update(elapsedTime: number) {
// technically disabled
if (this.animationRef.playCount === 0) return;
// the current time is after this animation (finished)
if (elapsedTime >= this.endPoint) {
this.#isPlaying = false;
this.#isFinished = true;
this.#playCount = this.animationRef.playCount;
this.#delayCount = this.animationRef.delayCount;
this.#progress = 1;
this.#overallProgress = 1;
this.#elapsedTime = this.endPoint - this.#startPoint;
const isReverse = this.animationRef.direction === Direction.Reverse || this.animationRef.direction === Direction.Alternate;
this.#value = isReverse ? this.animationRef.from : this.animationRef.to;
return;
}
// the current time is before this animation (not started yet)
if (elapsedTime < this.#startPoint) {
this.#isPlaying = false;
this.#isFinished = false;
this.#playCount = 0;
this.#delayCount = 0;
this.#progress = 0;
this.#overallProgress = 0;
this.#elapsedTime = 0;
this.#value = this.#isReverse ? this.animationRef.to : this.animationRef.from;
return;
}
// the current time is in this animation (playing)
this.#isPlaying = true;
this.#overallProgress = normalizePercentage((elapsedTime - this.#startPoint) / (this.endPoint - this.#startPoint));
this.#calculateValues(elapsedTime);
}
/**
* Set or update the animation values.
*
* ⚠️ **Warning** ⚠️ This method will throw an error if the animation values are invalid.
*/
public Set(animation: Partial<AnimationPreparedOptions>) {
Object.assign(this.animationRef, animation);
validateAnimationValues(this.animationRef);
}
#calculateValues(elapsedTime: number): void {
const withDelayCount = this.animationRef.delayCount;
const withoutDelayLength = this.animationRef.duration;
const withoutDelayTotalLength = withoutDelayLength * (this.animationRef.playCount - withDelayCount);
const withDelayLength = this.animationRef.duration + this.animationRef.delay;
const withDelayTotalLength = withDelayLength * withDelayCount + this.animationRef.delay * withDelayCount;
const totalLength = withDelayTotalLength + withoutDelayTotalLength;
const targetLength = totalLength * this.#overallProgress;
// falls under with delay parts
if (withDelayTotalLength && targetLength <= withDelayTotalLength) {
const at = clamp(Math.floor(targetLength / withDelayLength), 0, withDelayCount - 1);
const delay = withDelayCount === 0 ? 0 : this.animationRef.delay;
this.#start = this.#startPoint + withDelayLength * at + delay;
this.#delayCount = at + 1;
this.#playCount = at + 1;
// falls under without delay parts
} else {
const remainingLength = targetLength - withDelayTotalLength;
const at = withDelayCount + clamp(Math.floor(remainingLength / withoutDelayLength), 0, this.animationRef.playCount - 1);
this.#start = this.#startPoint + withDelayTotalLength + withoutDelayLength * (at - withDelayCount);
this.#delayCount = withDelayCount;
this.#playCount = at + 1;
}
this.#end = this.#start + this.animationRef.duration;
this.#elapsedTime = elapsedTime - this.#start;
this.#progress = normalizePercentage(this.#elapsedTime / (this.#end - this.#start));
const internalProgress = this.#calculateProgress();
this.#value = this.#startValue + (this.#endValue - this.#startValue) * this.animationRef.ease(internalProgress);
}
/** - Calculate the internal progress relative to the direction. */
#calculateProgress(): number {
if (!this.#isAlternate) return this.#progress;
const progress = (this.#progress <= 0.5 ? this.#progress : this.#progress - 0.5) * 2;
this.#alternateLap = this.#progress <= 0.5 ? 0 : 1;
// first lap
if (this.#alternateLap === 0) {
this.#startValue = this.#isReverse ? this.animationRef.to : this.animationRef.from;
this.#endValue = this.#isReverse ? this.animationRef.from : this.animationRef.to;
return progress;
}
// second lap
this.#startValue = this.#isReverse ? this.animationRef.from : this.animationRef.to;
this.#endValue = this.#isReverse ? this.animationRef.to : this.animationRef.from;
return progress;
}
}