animejs
Version:
JavaScript animation engine
1,251 lines (1,235 loc) • 278 kB
JavaScript
/**
* anime.js - ESM
* @version v4.0.0
* @author Julian Garnier
* @license MIT
* @copyright (c) 2025 Julian Garnier
* @see https://animejs.com
*/
// Environments
// TODO: Do we need to check if we're running inside a worker ?
const isBrowser = typeof window !== 'undefined';
/** @type {Object|Null} */
const win = isBrowser ? window : null;
/** @type {Document} */
const doc = isBrowser ? document : null;
// Enums
/** @enum {Number} */
const tweenTypes = {
OBJECT: 0,
ATTRIBUTE: 1,
CSS: 2,
TRANSFORM: 3,
CSS_VAR: 4,
};
/** @enum {Number} */
const valueTypes = {
NUMBER: 0,
UNIT: 1,
COLOR: 2,
COMPLEX: 3,
};
/** @enum {Number} */
const tickModes = {
NONE: 0,
AUTO: 1,
FORCE: 2,
};
/** @enum {Number} */
const compositionTypes = {
replace: 0,
none: 1,
blend: 2,
};
// Cache symbols
const isRegisteredTargetSymbol = Symbol();
const isDomSymbol = Symbol();
const isSvgSymbol = Symbol();
const transformsSymbol = Symbol();
const morphPointsSymbol = Symbol();
const proxyTargetSymbol = Symbol();
// Numbers
const minValue = 1e-11;
const maxValue = 1e12;
const K = 1e3;
const maxFps = 120;
// Strings
const emptyString = '';
const shortTransforms = new Map();
shortTransforms.set('x', 'translateX');
shortTransforms.set('y', 'translateY');
shortTransforms.set('z', 'translateZ');
const validTransforms = [
'translateX',
'translateY',
'translateZ',
'rotate',
'rotateX',
'rotateY',
'rotateZ',
'scale',
'scaleX',
'scaleY',
'scaleZ',
'skew',
'skewX',
'skewY',
'perspective',
'matrix',
'matrix3d',
];
const transformsFragmentStrings = validTransforms.reduce((a, v) => ({ ...a, [v]: v + '(' }), {});
// Functions
/** @return {void} */
const noop = () => { };
// Regex
const hexTestRgx = /(^#([\da-f]{3}){1,2}$)|(^#([\da-f]{4}){1,2}$)/i;
const rgbExecRgx = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i;
const rgbaExecRgx = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i;
const hslExecRgx = /hsl\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*\)/i;
const hslaExecRgx = /hsla\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)\s*\)/i;
// export const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/g;
const digitWithExponentRgx = /[-+]?\d*\.?\d+(?:e[-+]?\d)?/gi;
// export const unitsExecRgx = /^([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)+([a-z]+|%)$/i;
const unitsExecRgx = /^([-+]?\d*\.?\d+(?:e[-+]?\d+)?)([a-z]+|%)$/i;
const lowerCaseRgx = /([a-z])([A-Z])/g;
const transformsExecRgx = /(\w+)(\([^)]+\)+)/g; // Match inline transforms with cacl() values, returns the value wrapped in ()
const relativeValuesExecRgx = /(\*=|\+=|-=)/;
/** @type {DefaultsParams} */
const defaults = {
id: null,
keyframes: null,
playbackEase: null,
playbackRate: 1,
frameRate: maxFps,
loop: 0,
reversed: false,
alternate: false,
autoplay: true,
duration: K,
delay: 0,
loopDelay: 0,
ease: 'out(2)',
composition: compositionTypes.replace,
modifier: v => v,
onBegin: noop,
onBeforeUpdate: noop,
onUpdate: noop,
onLoop: noop,
onPause: noop,
onComplete: noop,
onRender: noop,
};
const globals = {
/** @type {DefaultsParams} */
defaults,
/** @type {Document|DOMTarget} */
root: doc,
/** @type {Scope} */
scope: null,
/** @type {Number} */
precision: 4,
/** @type {Number} */
timeScale: 1,
/** @type {Number} */
tickThreshold: 200,
};
const globalVersions = { version: '4.0.0', engine: null };
if (isBrowser) {
if (!win.AnimeJS)
win.AnimeJS = [];
win.AnimeJS.push(globalVersions);
}
// Strings
/**
* @param {String} str
* @return {String}
*/
const toLowerCase = str => str.replace(lowerCaseRgx, '$1-$2').toLowerCase();
/**
* Prioritize this method instead of regex when possible
* @param {String} str
* @param {String} sub
* @return {Boolean}
*/
const stringStartsWith = (str, sub) => str.indexOf(sub) === 0;
// Time
// Note: Date.now is used instead of performance.now since it is precise enough for timings calculations, performs slightly faster and works in Node.js environement.
const now = Date.now;
// Types checkers
const isArr = Array.isArray;
/**@param {any} a @return {a is Record<String, any>} */
const isObj = a => a && a.constructor === Object;
/**@param {any} a @return {a is Number} */
const isNum = a => typeof a === 'number' && !isNaN(a);
/**@param {any} a @return {a is String} */
const isStr = a => typeof a === 'string';
/**@param {any} a @return {a is Function} */
const isFnc = a => typeof a === 'function';
/**@param {any} a @return {a is undefined} */
const isUnd = a => typeof a === 'undefined';
/**@param {any} a @return {a is null | undefined} */
const isNil = a => isUnd(a) || a === null;
/**@param {any} a @return {a is SVGElement} */
const isSvg = a => isBrowser && a instanceof SVGElement;
/**@param {any} a @return {Boolean} */
const isHex = a => hexTestRgx.test(a);
/**@param {any} a @return {Boolean} */
const isRgb = a => stringStartsWith(a, 'rgb');
/**@param {any} a @return {Boolean} */
const isHsl = a => stringStartsWith(a, 'hsl');
/**@param {any} a @return {Boolean} */
const isCol = a => isHex(a) || isRgb(a) || isHsl(a);
/**@param {any} a @return {Boolean} */
const isKey = a => !globals.defaults.hasOwnProperty(a);
// Number
/**
* @param {Number|String} str
* @return {Number}
*/
const parseNumber = str => isStr(str) ?
parseFloat(/** @type {String} */ (str)) :
/** @type {Number} */ (str);
// Math
const pow = Math.pow;
const sqrt = Math.sqrt;
const sin = Math.sin;
const cos = Math.cos;
const abs = Math.abs;
const exp = Math.exp;
const ceil = Math.ceil;
const floor = Math.floor;
const asin = Math.asin;
const max = Math.max;
const atan2 = Math.atan2;
const PI = Math.PI;
const _round = Math.round;
/**
* @param {Number} v
* @param {Number} min
* @param {Number} max
* @return {Number}
*/
const clamp = (v, min, max) => v < min ? min : v > max ? max : v;
const powCache = {};
/**
* @param {Number} v
* @param {Number} decimalLength
* @return {Number}
*/
const round = (v, decimalLength) => {
if (decimalLength < 0)
return v;
if (!decimalLength)
return _round(v);
let p = powCache[decimalLength];
if (!p)
p = powCache[decimalLength] = 10 ** decimalLength;
return _round(v * p) / p;
};
/**
* @param {Number} v
* @param {Number|Array<Number>} increment
* @return {Number}
*/
const snap = (v, increment) => isArr(increment) ? increment.reduce((closest, cv) => (abs(cv - v) < abs(closest - v) ? cv : closest)) : increment ? _round(v / increment) * increment : v;
/**
* @param {Number} start
* @param {Number} end
* @param {Number} progress
* @return {Number}
*/
const interpolate = (start, end, progress) => start + (end - start) * progress;
/**
* @param {Number} v
* @return {Number}
*/
const clampInfinity = v => v === Infinity ? maxValue : v === -Infinity ? -1e12 : v;
/**
* @param {Number} v
* @return {Number}
*/
const clampZero = v => v < minValue ? minValue : v;
// Arrays
/**
* @template T
* @param {T[]} a
* @return {T[]}
*/
const cloneArray = a => isArr(a) ? [...a] : a;
// Objects
/**
* @template T
* @template U
* @param {T} o1
* @param {U} o2
* @return {T & U}
*/
const mergeObjects = (o1, o2) => {
const merged = /** @type {T & U} */ ({ ...o1 });
for (let p in o2) {
const o1p = /** @type {T & U} */ (o1)[p];
merged[p] = isUnd(o1p) ? /** @type {T & U} */ (o2)[p] : o1p;
}
return merged;
};
// Linked lists
/**
* @param {Object} parent
* @param {Function} callback
* @param {Boolean} [reverse]
* @param {String} [prevProp]
* @param {String} [nextProp]
* @return {void}
*/
const forEachChildren = (parent, callback, reverse, prevProp = '_prev', nextProp = '_next') => {
let next = parent._head;
let adjustedNextProp = nextProp;
if (reverse) {
next = parent._tail;
adjustedNextProp = prevProp;
}
while (next) {
const currentNext = next[adjustedNextProp];
callback(next);
next = currentNext;
}
};
/**
* @param {Object} parent
* @param {Object} child
* @param {String} [prevProp]
* @param {String} [nextProp]
* @return {void}
*/
const removeChild = (parent, child, prevProp = '_prev', nextProp = '_next') => {
const prev = child[prevProp];
const next = child[nextProp];
prev ? prev[nextProp] = next : parent._head = next;
next ? next[prevProp] = prev : parent._tail = prev;
child[prevProp] = null;
child[nextProp] = null;
};
/**
* @param {Object} parent
* @param {Object} child
* @param {Function} [sortMethod]
* @param {String} prevProp
* @param {String} nextProp
* @return {void}
*/
const addChild = (parent, child, sortMethod, prevProp = '_prev', nextProp = '_next') => {
let prev = parent._tail;
while (prev && sortMethod && sortMethod(prev, child))
prev = prev[prevProp];
const next = prev ? prev[nextProp] : parent._head;
prev ? prev[nextProp] = child : parent._head = child;
next ? next[prevProp] = child : parent._tail = child;
child[prevProp] = prev;
child[nextProp] = next;
};
/*
* Base class to control framerate and playback rate.
* Inherited by Engine, Timer, Animation and Timeline.
*/
class Clock {
/** @param {Number} [initTime] */
constructor(initTime = 0) {
/** @type {Number} */
this.deltaTime = 0;
/** @type {Number} */
this._currentTime = initTime;
/** @type {Number} */
this._elapsedTime = initTime;
/** @type {Number} */
this._startTime = initTime;
/** @type {Number} */
this._lastTime = initTime;
/** @type {Number} */
this._scheduledTime = 0;
/** @type {Number} */
this._frameDuration = round(K / maxFps, 0);
/** @type {Number} */
this._fps = maxFps;
/** @type {Number} */
this._speed = 1;
/** @type {Boolean} */
this._hasChildren = false;
/** @type {Tickable|Tween} */
this._head = null;
/** @type {Tickable|Tween} */
this._tail = null;
}
get fps() {
return this._fps;
}
set fps(frameRate) {
const previousFrameDuration = this._frameDuration;
const fr = +frameRate;
const fps = fr < minValue ? minValue : fr;
const frameDuration = round(K / fps, 0);
this._fps = fps;
this._frameDuration = frameDuration;
this._scheduledTime += frameDuration - previousFrameDuration;
}
get speed() {
return this._speed;
}
set speed(playbackRate) {
const pbr = +playbackRate;
this._speed = pbr < minValue ? minValue : pbr;
}
/**
* @param {Number} time
* @return {tickModes}
*/
requestTick(time) {
const scheduledTime = this._scheduledTime;
const elapsedTime = this._elapsedTime;
this._elapsedTime += (time - elapsedTime);
// If the elapsed time is lower than the scheduled time
// this means not enough time has passed to hit one frameDuration
// so skip that frame
if (elapsedTime < scheduledTime)
return tickModes.NONE;
const frameDuration = this._frameDuration;
const frameDelta = elapsedTime - scheduledTime;
// Ensures that _scheduledTime progresses in steps of at least 1 frameDuration.
// Skips ahead if the actual elapsed time is higher.
this._scheduledTime += frameDelta < frameDuration ? frameDuration : frameDelta;
return tickModes.AUTO;
}
/**
* @param {Number} time
* @return {Number}
*/
computeDeltaTime(time) {
const delta = time - this._lastTime;
this.deltaTime = delta;
this._lastTime = time;
return delta;
}
}
/**
* @param {Tickable} tickable
* @param {Number} time
* @param {Number} muteCallbacks
* @param {Number} internalRender
* @param {tickModes} tickMode
* @return {Number}
*/
const render = (tickable, time, muteCallbacks, internalRender, tickMode) => {
const parent = tickable.parent;
const duration = tickable.duration;
const completed = tickable.completed;
const iterationDuration = tickable.iterationDuration;
const iterationCount = tickable.iterationCount;
const _currentIteration = tickable._currentIteration;
const _loopDelay = tickable._loopDelay;
const _reversed = tickable._reversed;
const _alternate = tickable._alternate;
const _hasChildren = tickable._hasChildren;
const tickableDelay = tickable._delay;
const tickablePrevAbsoluteTime = tickable._currentTime; // TODO: rename ._currentTime to ._absoluteCurrentTime
const tickableEndTime = tickableDelay + iterationDuration;
const tickableAbsoluteTime = time - tickableDelay;
const tickablePrevTime = clamp(tickablePrevAbsoluteTime, -tickableDelay, duration);
const tickableCurrentTime = clamp(tickableAbsoluteTime, -tickableDelay, duration);
const deltaTime = tickableAbsoluteTime - tickablePrevAbsoluteTime;
const isCurrentTimeAboveZero = tickableCurrentTime > 0;
const isCurrentTimeEqualOrAboveDuration = tickableCurrentTime >= duration;
const isSetter = duration <= minValue;
const forcedTick = tickMode === tickModes.FORCE;
let isOdd = 0;
let iterationElapsedTime = tickableAbsoluteTime;
// Render checks
// Used to also check if the children have rendered in order to trigger the onRender callback on the parent timer
let hasRendered = 0;
// Execute the "expensive" iterations calculations only when necessary
if (iterationCount > 1) {
// bitwise NOT operator seems to be generally faster than Math.floor() across browsers
const currentIteration = ~~(tickableCurrentTime / (iterationDuration + (isCurrentTimeEqualOrAboveDuration ? 0 : _loopDelay)));
tickable._currentIteration = clamp(currentIteration, 0, iterationCount);
// Prevent the iteration count to go above the max iterations when reaching the end of the animation
if (isCurrentTimeEqualOrAboveDuration)
tickable._currentIteration--;
isOdd = tickable._currentIteration % 2;
iterationElapsedTime = tickableCurrentTime % (iterationDuration + _loopDelay) || 0;
}
// Checks if exactly one of _reversed and (_alternate && isOdd) is true
const isReversed = _reversed ^ (_alternate && isOdd);
const _ease = /** @type {Renderable} */ (tickable)._ease;
let iterationTime = isCurrentTimeEqualOrAboveDuration ? isReversed ? 0 : duration : isReversed ? iterationDuration - iterationElapsedTime : iterationElapsedTime;
if (_ease)
iterationTime = iterationDuration * _ease(iterationTime / iterationDuration) || 0;
const isRunningBackwards = (parent ? parent.backwards : tickableAbsoluteTime < tickablePrevAbsoluteTime) ? !isReversed : !!isReversed;
tickable._currentTime = tickableAbsoluteTime;
tickable._iterationTime = iterationTime;
tickable.backwards = isRunningBackwards;
if (isCurrentTimeAboveZero && !tickable.began) {
tickable.began = true;
if (!muteCallbacks && !(parent && (isRunningBackwards || !parent.began))) {
tickable.onBegin(/** @type {CallbackArgument} */ (tickable));
}
}
else if (tickableAbsoluteTime <= 0) {
tickable.began = false;
}
// Only triggers onLoop for tickable without children, otherwise call the the onLoop callback in the tick function
// Make sure to trigger the onLoop before rendering to allow .refresh() to pickup the current values
if (!muteCallbacks && !_hasChildren && isCurrentTimeAboveZero && tickable._currentIteration !== _currentIteration) {
tickable.onLoop(/** @type {CallbackArgument} */ (tickable));
}
if (forcedTick ||
tickMode === tickModes.AUTO && (time >= tickableDelay && time <= tickableEndTime || // Normal render
time <= tickableDelay && tickablePrevTime > tickableDelay || // Playhead is before the animation start time so make sure the animation is at its initial state
time >= tickableEndTime && tickablePrevTime !== duration // Playhead is after the animation end time so make sure the animation is at its end state
) ||
iterationTime >= tickableEndTime && tickablePrevTime !== duration ||
iterationTime <= tickableDelay && tickablePrevTime > 0 ||
time <= tickablePrevTime && tickablePrevTime === duration && completed || // Force a render if a seek occurs on an completed animation
isCurrentTimeEqualOrAboveDuration && !completed && isSetter // This prevents 0 duration tickables to be skipped
) {
if (isCurrentTimeAboveZero) {
// Trigger onUpdate callback before rendering
tickable.computeDeltaTime(tickablePrevTime);
if (!muteCallbacks)
tickable.onBeforeUpdate(/** @type {CallbackArgument} */ (tickable));
}
// Start tweens rendering
if (!_hasChildren) {
// Time has jumped more than globals.tickThreshold so consider this tick manual
const forcedRender = forcedTick || (isRunningBackwards ? deltaTime * -1 : deltaTime) >= globals.tickThreshold;
const absoluteTime = tickable._offset + (parent ? parent._offset : 0) + tickableDelay + iterationTime;
// Only Animation can have tweens, Timer returns undefined
let tween = /** @type {Tween} */ ( /** @type {JSAnimation} */(tickable)._head);
let tweenTarget;
let tweenStyle;
let tweenTargetTransforms;
let tweenTargetTransformsProperties;
let tweenTransformsNeedUpdate = 0;
while (tween) {
const tweenComposition = tween._composition;
const tweenCurrentTime = tween._currentTime;
const tweenChangeDuration = tween._changeDuration;
const tweenAbsEndTime = tween._absoluteStartTime + tween._changeDuration;
const tweenNextRep = tween._nextRep;
const tweenPrevRep = tween._prevRep;
const tweenHasComposition = tweenComposition !== compositionTypes.none;
if ((forcedRender || ((tweenCurrentTime !== tweenChangeDuration || absoluteTime <= tweenAbsEndTime + (tweenNextRep ? tweenNextRep._delay : 0)) &&
(tweenCurrentTime !== 0 || absoluteTime >= tween._absoluteStartTime))) && (!tweenHasComposition || (!tween._isOverridden &&
(!tween._isOverlapped || absoluteTime <= tweenAbsEndTime) &&
(!tweenNextRep || (tweenNextRep._isOverridden || absoluteTime <= tweenNextRep._absoluteStartTime)) &&
(!tweenPrevRep || (tweenPrevRep._isOverridden || (absoluteTime >= (tweenPrevRep._absoluteStartTime + tweenPrevRep._changeDuration) + tween._delay)))))) {
const tweenNewTime = tween._currentTime = clamp(iterationTime - tween._startTime, 0, tweenChangeDuration);
const tweenProgress = tween._ease(tweenNewTime / tween._updateDuration);
const tweenModifier = tween._modifier;
const tweenValueType = tween._valueType;
const tweenType = tween._tweenType;
const tweenIsObject = tweenType === tweenTypes.OBJECT;
const tweenIsNumber = tweenValueType === valueTypes.NUMBER;
// Only round the in-between frames values if the final value is a string
const tweenPrecision = (tweenIsNumber && tweenIsObject) || tweenProgress === 0 || tweenProgress === 1 ? -1 : globals.precision;
// Recompose tween value
/** @type {String|Number} */
let value;
/** @type {Number} */
let number;
if (tweenIsNumber) {
value = number = /** @type {Number} */ (tweenModifier(round(interpolate(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision)));
}
else if (tweenValueType === valueTypes.UNIT) {
// Rounding the values speed up string composition
number = /** @type {Number} */ (tweenModifier(round(interpolate(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision)));
value = `${number}${tween._unit}`;
}
else if (tweenValueType === valueTypes.COLOR) {
const fn = tween._fromNumbers;
const tn = tween._toNumbers;
const r = round(clamp(/** @type {Number} */ (tweenModifier(interpolate(fn[0], tn[0], tweenProgress))), 0, 255), 0);
const g = round(clamp(/** @type {Number} */ (tweenModifier(interpolate(fn[1], tn[1], tweenProgress))), 0, 255), 0);
const b = round(clamp(/** @type {Number} */ (tweenModifier(interpolate(fn[2], tn[2], tweenProgress))), 0, 255), 0);
const a = clamp(/** @type {Number} */ (tweenModifier(round(interpolate(fn[3], tn[3], tweenProgress), tweenPrecision))), 0, 1);
value = `rgba(${r},${g},${b},${a})`;
if (tweenHasComposition) {
const ns = tween._numbers;
ns[0] = r;
ns[1] = g;
ns[2] = b;
ns[3] = a;
}
}
else if (tweenValueType === valueTypes.COMPLEX) {
value = tween._strings[0];
for (let j = 0, l = tween._toNumbers.length; j < l; j++) {
const n = /** @type {Number} */ (tweenModifier(round(interpolate(tween._fromNumbers[j], tween._toNumbers[j], tweenProgress), tweenPrecision)));
const s = tween._strings[j + 1];
value += `${s ? n + s : n}`;
if (tweenHasComposition) {
tween._numbers[j] = n;
}
}
}
// For additive tweens and Animatables
if (tweenHasComposition) {
tween._number = number;
}
if (!internalRender && tweenComposition !== compositionTypes.blend) {
const tweenProperty = tween.property;
tweenTarget = tween.target;
if (tweenIsObject) {
tweenTarget[tweenProperty] = value;
}
else if (tweenType === tweenTypes.ATTRIBUTE) {
/** @type {DOMTarget} */ (tweenTarget).setAttribute(tweenProperty, /** @type {String} */ (value));
}
else {
tweenStyle = /** @type {DOMTarget} */ (tweenTarget).style;
if (tweenType === tweenTypes.TRANSFORM) {
if (tweenTarget !== tweenTargetTransforms) {
tweenTargetTransforms = tweenTarget;
// NOTE: Referencing the cachedTransforms in the tween property directly can be a little bit faster but appears to increase memory usage.
tweenTargetTransformsProperties = tweenTarget[transformsSymbol];
}
tweenTargetTransformsProperties[tweenProperty] = value;
tweenTransformsNeedUpdate = 1;
}
else if (tweenType === tweenTypes.CSS) {
tweenStyle[tweenProperty] = value;
}
else if (tweenType === tweenTypes.CSS_VAR) {
tweenStyle.setProperty(tweenProperty, /** @type {String} */ (value));
}
}
if (isCurrentTimeAboveZero)
hasRendered = 1;
}
else {
// Used for composing timeline tweens without having to do a real render
tween._value = value;
}
}
// NOTE: Possible improvement: Use translate(x,y) / translate3d(x,y,z) syntax
// to reduce memory usage on string composition
if (tweenTransformsNeedUpdate && tween._renderTransforms) {
let str = emptyString;
for (let key in tweenTargetTransformsProperties) {
str += `${transformsFragmentStrings[key]}${tweenTargetTransformsProperties[key]}) `;
}
tweenStyle.transform = str;
tweenTransformsNeedUpdate = 0;
}
tween = tween._next;
}
if (!muteCallbacks && hasRendered) {
/** @type {JSAnimation} */ (tickable).onRender(/** @type {JSAnimation} */ (tickable));
}
}
if (!muteCallbacks && isCurrentTimeAboveZero) {
tickable.onUpdate(/** @type {CallbackArgument} */ (tickable));
}
}
// End tweens rendering
// Handle setters on timeline differently and allow re-trigering the onComplete callback when seeking backwards
if (parent && isSetter) {
if (!muteCallbacks && ((parent.began && !isRunningBackwards && tickableAbsoluteTime >= duration && !completed) ||
(isRunningBackwards && tickableAbsoluteTime <= minValue && completed))) {
tickable.onComplete(/** @type {CallbackArgument} */ (tickable));
tickable.completed = !isRunningBackwards;
}
// If currentTime is both above 0 and at least equals to duration, handles normal onComplete or infinite loops
}
else if (isCurrentTimeAboveZero && isCurrentTimeEqualOrAboveDuration) {
if (iterationCount === Infinity) {
// Offset the tickable _startTime with its duration to reset _currentTime to 0 and continue the infinite timer
tickable._startTime += tickable.duration;
}
else if (tickable._currentIteration >= iterationCount - 1) {
// By setting paused to true, we tell the engine loop to not render this tickable and removes it from the list on the next tick
tickable.paused = true;
if (!completed && !_hasChildren) {
// If the tickable has children, triggers onComplete() only when all children have completed in the tick function
tickable.completed = true;
if (!muteCallbacks && !(parent && (isRunningBackwards || !parent.began))) {
tickable.onComplete(/** @type {CallbackArgument} */ (tickable));
tickable._resolve(/** @type {CallbackArgument} */ (tickable));
}
}
}
// Otherwise set the completed flag to false
}
else {
tickable.completed = false;
}
// NOTE: hasRendered * direction (negative for backwards) this way we can remove the tickable.backwards property completly ?
return hasRendered;
};
/**
* @param {Tickable} tickable
* @param {Number} time
* @param {Number} muteCallbacks
* @param {Number} internalRender
* @param {Number} tickMode
* @return {void}
*/
const tick = (tickable, time, muteCallbacks, internalRender, tickMode) => {
const _currentIteration = tickable._currentIteration;
render(tickable, time, muteCallbacks, internalRender, tickMode);
if (tickable._hasChildren) {
const tl = /** @type {Timeline} */ (tickable);
const tlIsRunningBackwards = tl.backwards;
const tlChildrenTime = internalRender ? time : tl._iterationTime;
const tlCildrenTickTime = now();
let tlChildrenHasRendered = 0;
let tlChildrenHaveCompleted = true;
// If the timeline has looped forward, we need to manually triggers children skipped callbacks
if (!internalRender && tl._currentIteration !== _currentIteration) {
const tlIterationDuration = tl.iterationDuration;
forEachChildren(tl, (/** @type {JSAnimation} */ child) => {
if (!tlIsRunningBackwards) {
// Force an internal render to trigger the callbacks if the child has not completed on loop
if (!child.completed && !child.backwards && child._currentTime < child.iterationDuration) {
render(child, tlIterationDuration, muteCallbacks, 1, tickModes.FORCE);
}
// Reset their began and completed flags to allow retrigering callbacks on the next iteration
child.began = false;
child.completed = false;
}
else {
const childDuration = child.duration;
const childStartTime = child._offset + child._delay;
const childEndTime = childStartTime + childDuration;
// Triggers the onComplete callback on reverse for children on the edges of the timeline
if (!muteCallbacks && childDuration <= minValue && (!childStartTime || childEndTime === tlIterationDuration)) {
child.onComplete(child);
}
}
});
if (!muteCallbacks)
tl.onLoop(/** @type {CallbackArgument} */ (tl));
}
forEachChildren(tl, (/** @type {JSAnimation} */ child) => {
const childTime = round((tlChildrenTime - child._offset) * child._speed, 12); // Rounding is needed when using seconds
const childTickMode = child._fps < tl._fps ? child.requestTick(tlCildrenTickTime) : tickMode;
tlChildrenHasRendered += render(child, childTime, muteCallbacks, internalRender, childTickMode);
if (!child.completed && tlChildrenHaveCompleted)
tlChildrenHaveCompleted = false;
}, tlIsRunningBackwards);
// Renders on timeline are triggered by its children so it needs to be set after rendering the children
if (!muteCallbacks && tlChildrenHasRendered)
tl.onRender(/** @type {CallbackArgument} */ (tl));
// Triggers the timeline onComplete() once all chindren all completed and the current time has reached the end
if (tlChildrenHaveCompleted && tl._currentTime >= tl.duration) {
// Make sure the paused flag is false in case it has been skipped in the render function
tl.paused = true;
if (!tl.completed) {
tl.completed = true;
if (!muteCallbacks) {
tl.onComplete(/** @type {CallbackArgument} */ (tl));
tl._resolve(/** @type {CallbackArgument} */ (tl));
}
}
}
}
};
const additive = {
animation: null,
update: noop,
};
/**
* @typedef AdditiveAnimation
* @property {Number} duration
* @property {Number} _offset
* @property {Number} _delay
* @property {Tween} _head
* @property {Tween} _tail
*/
/**
* @param {TweenAdditiveLookups} lookups
* @return {AdditiveAnimation}
*/
const addAdditiveAnimation = lookups => {
let animation = additive.animation;
if (!animation) {
animation = {
duration: minValue,
computeDeltaTime: noop,
_offset: 0,
_delay: 0,
_head: null,
_tail: null,
};
additive.animation = animation;
additive.update = () => {
lookups.forEach(propertyAnimation => {
for (let propertyName in propertyAnimation) {
const tweens = propertyAnimation[propertyName];
const lookupTween = tweens._head;
if (lookupTween) {
const valueType = lookupTween._valueType;
const additiveValues = valueType === valueTypes.COMPLEX || valueType === valueTypes.COLOR ? cloneArray(lookupTween._fromNumbers) : null;
let additiveValue = lookupTween._fromNumber;
let tween = tweens._tail;
while (tween && tween !== lookupTween) {
if (additiveValues) {
for (let i = 0, l = tween._numbers.length; i < l; i++)
additiveValues[i] += tween._numbers[i];
}
else {
additiveValue += tween._number;
}
tween = tween._prevAdd;
}
lookupTween._toNumber = additiveValue;
lookupTween._toNumbers = additiveValues;
}
}
});
// TODO: Avoid polymorphism here, idealy the additive animation should be a regular animation with a higher priority in the render loop
render(animation, 1, 1, 0, tickModes.FORCE);
};
}
return animation;
};
const engineTickMethod = isBrowser ? requestAnimationFrame : setImmediate;
const engineCancelMethod = isBrowser ? cancelAnimationFrame : clearImmediate;
class Engine extends Clock {
/** @param {Number} [initTime] */
constructor(initTime) {
super(initTime);
this.useDefaultMainLoop = true;
this.pauseOnDocumentHidden = true;
/** @type {DefaultsParams} */
this.defaults = defaults;
this.paused = isBrowser && doc.hidden ? true : false;
/** @type {Number|NodeJS.Immediate} */
this.reqId = null;
}
update() {
const time = this._currentTime = now();
if (this.requestTick(time)) {
this.computeDeltaTime(time);
const engineSpeed = this._speed;
const engineFps = this._fps;
let activeTickable = /** @type {Tickable} */ (this._head);
while (activeTickable) {
const nextTickable = activeTickable._next;
if (!activeTickable.paused) {
tick(activeTickable, (time - activeTickable._startTime) * activeTickable._speed * engineSpeed, 0, // !muteCallbacks
0, // !internalRender
activeTickable._fps < engineFps ? activeTickable.requestTick(time) : tickModes.AUTO);
}
else {
removeChild(this, activeTickable);
this._hasChildren = !!this._tail;
activeTickable._running = false;
if (activeTickable.completed && !activeTickable._cancelled) {
activeTickable.cancel();
}
}
activeTickable = nextTickable;
}
additive.update();
}
}
wake() {
if (this.useDefaultMainLoop && !this.reqId && !this.paused) {
this.reqId = engineTickMethod(tickEngine);
}
return this;
}
pause() {
this.paused = true;
return killEngine();
}
resume() {
if (!this.paused)
return;
this.paused = false;
forEachChildren(this, (/** @type {Tickable} */ child) => child.resetTime());
return this.wake();
}
// Getter and setter for speed
get speed() {
return this._speed * (globals.timeScale === 1 ? 1 : K);
}
set speed(playbackRate) {
this._speed = playbackRate * globals.timeScale;
forEachChildren(this, (/** @type {Tickable} */ child) => child.speed = child._speed);
}
// Getter and setter for timeUnit
get timeUnit() {
return globals.timeScale === 1 ? 'ms' : 's';
}
;
set timeUnit(unit) {
const secondsScale = 0.001;
const isSecond = unit === 's';
const newScale = isSecond ? secondsScale : 1;
if (globals.timeScale !== newScale) {
globals.timeScale = newScale;
globals.tickThreshold = 200 * newScale;
const scaleFactor = isSecond ? secondsScale : K;
/** @type {Number} */
(this.defaults.duration) *= scaleFactor;
this._speed *= scaleFactor;
}
}
// Getter and setter for precision
get precision() {
return globals.precision;
}
set precision(precision) {
globals.precision = precision;
}
}
const engine = /*#__PURE__*/ (() => {
const engine = new Engine(now());
if (isBrowser) {
globalVersions.engine = engine;
doc.addEventListener('visibilitychange', () => {
if (!engine.pauseOnDocumentHidden)
return;
doc.hidden ? engine.pause() : engine.resume();
});
}
return engine;
})();
const tickEngine = () => {
if (engine._head) {
engine.reqId = engineTickMethod(tickEngine);
engine.update();
}
else {
engine.reqId = 0;
}
};
const killEngine = () => {
engineCancelMethod(/** @type {NodeJS.Immediate & Number} */ (engine.reqId));
engine.reqId = 0;
return engine;
};
/**
* @param {DOMTarget} target
* @param {String} propName
* @param {Object} animationInlineStyles
* @return {String}
*/
const parseInlineTransforms = (target, propName, animationInlineStyles) => {
const inlineTransforms = target.style.transform;
let inlinedStylesPropertyValue;
if (inlineTransforms) {
const cachedTransforms = target[transformsSymbol];
let t;
while (t = transformsExecRgx.exec(inlineTransforms)) {
const inlinePropertyName = t[1];
// const inlinePropertyValue = t[2];
const inlinePropertyValue = t[2].slice(1, -1);
cachedTransforms[inlinePropertyName] = inlinePropertyValue;
if (inlinePropertyName === propName) {
inlinedStylesPropertyValue = inlinePropertyValue;
// Store the new parsed inline styles if animationInlineStyles is provided
if (animationInlineStyles) {
animationInlineStyles[propName] = inlinePropertyValue;
}
}
}
}
return inlineTransforms && !isUnd(inlinedStylesPropertyValue) ? inlinedStylesPropertyValue :
stringStartsWith(propName, 'scale') ? '1' :
stringStartsWith(propName, 'rotate') || stringStartsWith(propName, 'skew') ? '0deg' : '0px';
};
/**
* @param {DOMTargetsParam|TargetsParam} v
* @return {NodeList|HTMLCollection}
*/
function getNodeList(v) {
const n = isStr(v) ? globals.root.querySelectorAll(v) : v;
if (n instanceof NodeList || n instanceof HTMLCollection)
return n;
}
/**
* @overload
* @param {DOMTargetsParam} targets
* @return {DOMTargetsArray}
*
* @overload
* @param {JSTargetsParam} targets
* @return {JSTargetsArray}
*
* @overload
* @param {TargetsParam} targets
* @return {TargetsArray}
*
* @param {DOMTargetsParam|JSTargetsParam|TargetsParam} targets
*/
function parseTargets(targets) {
if (isNil(targets))
return /** @type {TargetsArray} */ ([]);
if (isArr(targets)) {
const flattened = targets.flat(Infinity);
/** @type {TargetsArray} */
const parsed = [];
for (let i = 0, l = flattened.length; i < l; i++) {
const item = flattened[i];
if (!isNil(item)) {
const nodeList = getNodeList(item);
if (nodeList) {
for (let j = 0, jl = nodeList.length; j < jl; j++) {
const subItem = nodeList[j];
if (!isNil(subItem)) {
let isDuplicate = false;
for (let k = 0, kl = parsed.length; k < kl; k++) {
if (parsed[k] === subItem) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
parsed.push(subItem);
}
}
}
}
else {
let isDuplicate = false;
for (let j = 0, jl = parsed.length; j < jl; j++) {
if (parsed[j] === item) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
parsed.push(item);
}
}
}
}
return parsed;
}
if (!isBrowser)
return /** @type {JSTargetsArray} */ ([targets]);
const nodeList = getNodeList(targets);
if (nodeList)
return /** @type {DOMTargetsArray} */ (Array.from(nodeList));
return /** @type {TargetsArray} */ ([targets]);
}
/**
* @overload
* @param {DOMTargetsParam} targets
* @return {DOMTargetsArray}
*
* @overload
* @param {JSTargetsParam} targets
* @return {JSTargetsArray}
*
* @overload
* @param {TargetsParam} targets
* @return {TargetsArray}
*
* @param {DOMTargetsParam|JSTargetsParam|TargetsParam} targets
*/
function registerTargets(targets) {
const parsedTargetsArray = parseTargets(targets);
const parsedTargetsLength = parsedTargetsArray.length;
if (parsedTargetsLength) {
for (let i = 0; i < parsedTargetsLength; i++) {
const target = parsedTargetsArray[i];
if (!target[isRegisteredTargetSymbol]) {
target[isRegisteredTargetSymbol] = true;
const isSvgType = isSvg(target);
const isDom = /** @type {DOMTarget} */ (target).nodeType || isSvgType;
if (isDom) {
target[isDomSymbol] = true;
target[isSvgSymbol] = isSvgType;
target[transformsSymbol] = {};
}
}
}
}
return parsedTargetsArray;
}
/**
* @param {TargetsParam} path
* @return {SVGGeometryElement|undefined}
*/
const getPath = path => {
const parsedTargets = parseTargets(path);
const $parsedSvg = /** @type {SVGGeometryElement} */ (parsedTargets[0]);
if (!$parsedSvg || !isSvg($parsedSvg))
return;
return $parsedSvg;
};
/**
* @param {TargetsParam} path2
* @param {Number} [precision]
* @return {FunctionValue}
*/
const morphTo = (path2, precision = .33) => ($path1) => {
const $path2 = /** @type {SVGGeometryElement} */ (getPath(path2));
if (!$path2)
return;
const isPath = $path1.tagName === 'path';
const separator = isPath ? ' ' : ',';
const previousPoints = $path1[morphPointsSymbol];
if (previousPoints)
$path1.setAttribute(isPath ? 'd' : 'points', previousPoints);
let v1 = '', v2 = '';
if (!precision) {
v1 = $path1.getAttribute(isPath ? 'd' : 'points');
v2 = $path2.getAttribute(isPath ? 'd' : 'points');
}
else {
const length1 = /** @type {SVGGeometryElement} */ ($path1).getTotalLength();
const length2 = $path2.getTotalLength();
const maxPoints = Math.max(Math.ceil(length1 * precision), Math.ceil(length2 * precision));
for (let i = 0; i < maxPoints; i++) {
const t = i / (maxPoints - 1);
const pointOnPath1 = /** @type {SVGGeometryElement} */ ($path1).getPointAtLength(length1 * t);
const pointOnPath2 = $path2.getPointAtLength(length2 * t);
const prefix = isPath ? (i === 0 ? 'M' : 'L') : '';
v1 += prefix + round(pointOnPath1.x, 3) + separator + pointOnPath1.y + ' ';
v2 += prefix + round(pointOnPath2.x, 3) + separator + pointOnPath2.y + ' ';
}
}
$path1[morphPointsSymbol] = v2;
return [v1, v2];
};
/**
* @param {SVGGeometryElement} $el
* @param {Number} start
* @param {Number} end
* @return {Proxy}
*/
function createDrawableProxy($el, start, end) {
const strokeLineCap = getComputedStyle($el).strokeLinecap;
const pathLength = K;
let currentCap = strokeLineCap;
const proxy = new Proxy($el, {
get(target, property) {
const value = target[property];
if (property === proxyTargetSymbol)
return target;
if (property === 'setAttribute') {
/** @param {any[]} args */
return (...args) => {
if (args[0] === 'draw') {
const value = args[1];
const values = value.split(' ');
const v1 = +values[0];
const v2 = +values[1];
// TOTO: Benchmark if performing two slices is more performant than one split
// const spaceIndex = value.indexOf(' ');
// const v1 = round(+value.slice(0, spaceIndex), precision);
// const v2 = round(+value.slice(spaceIndex + 1), precision);
const os = v1 * -1e3;
const d1 = (v2 * pathLength) + os;
// Prevents linecap to smear by offsetting the dasharray length by 0.01% when v2 is not at max
const d2 = (pathLength + ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10) - d1);
// Handle cases where the cap is still visible when the line is completly hidden
if (strokeLineCap !== 'butt') {
const newCap = v1 === v2 ? 'butt' : strokeLineCap;
if (currentCap !== newCap) {
target.setAttribute('stroke-linecap', `${newCap}`);
currentCap = newCap;
}
}
target.setAttribute('stroke-dashoffset', `${os}`);
target.setAttribute('stroke-dasharray', `${d1} ${d2}`);
}
return Reflect.apply(value, target, args);
};
}
if (isFnc(value)) {
/** @param {any[]} args */
return (...args) => Reflect.apply(value, target, args);
}
else {
return value;
}
}
});
if ($el.getAttribute('pathLength') !== `${pathLength}`) {
$el.setAttribute('pathLength', `${pathLength}`);
proxy.setAttribute('draw', `${start} ${end}`);
}
return /** @type {typeof Proxy} */ ( /** @type {unknown} */(proxy));
}
/**
* @param {TargetsParam} selector
* @param {Number} [start=0]
* @param {Number} [end=0]
* @return {Array.<Proxy>}
*/
const createDrawable = (selector, start = 0, end = 0) => {
const els = /** @type {Array.<Proxy>} */ (( /** @type {unknown} */(parseTargets(selector))));
els.forEach(($el, i) => els[i] = createDrawableProxy(/** @type {SVGGeometryElement} */ ( /** @type {unknown} */($el)), start, end));
return els;
};
// Motion path animation
/**
* @param {SVGGeometryElement} $path
* @param {Number} progress
* @param {Number}lookup
* @return {DOMPoint}
*/
const getPathPoint = ($path, progress, lookup = 0) => {
return $path.getPointAtLength(progress + lookup >= 1 ? progress + lookup : 0);
};
/**
* @param {SVGGeometryElement} $path
* @param {String} pathProperty
* @return {FunctionValue}
*/
const getPathProgess = ($path, pathProperty) => {
return $el => {
const totalLength = +($path.getTotalLength());
const inSvg = $el[isSvgSymbol];
const ctm = $path.getCTM();
/** @type {TweenObjectValue} */
return {
from: 0,
to: totalLength,
/** @type {TweenModifier} */
modifier: progress => {
if (pathProperty === 'a') {
const p0 = getPathPoint($path, progress, -1);
const p1 = getPathPoint($path, progress, 1);
return atan2(p1.y - p0.y, p1.x - p0.x) * 180 / PI;
}
else {
const p = getPathPoint($path, progress, 0);
return pathProperty === 'x' ?
inSvg || !ctm ? p.x : p.x * ctm.a + p.y * ctm.c + ctm.e :
inSvg || !ctm ? p.y : p.x * ctm.b + p.y * ctm.d + ctm.f;
}
}
};
};
};
/**
* @param {TargetsParam} path
*/
const createMotionPath = path => {
const $path = getPath(path);
if (!$path)
return;
return {
translateX: getPathProgess($path, 'x'),
translateY: getPathProgess($path, 'y'),
rotate: getPathProgess($path, 'a'),
};
};
// Check for valid SVG attribute
const cssReservedProperties = ['opacity', 'rotate', 'overflow', 'color'];
/**
* @param {Target} el
* @param {String} propertyName
* @return {Boolean}
*/
const isValidSVGAttribute = (el, propertyName) => {
// Return early and use CSS opacity animation instead (already better default values (opacity: 1 instead of 0)) and rotate should be considered a transform
if (cssReservedProperties.includes(propertyName))
return false;
if (el.getAttribute(propertyName) || propertyName in el) {
if (propertyName === '