UNPKG

animejs

Version:

JavaScript animation engine

1,240 lines (1,223 loc) 303 kB
/** * anime.js - ESM * @version v4.1.3 * @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 {Window & {AnimeJS: Array}|null} */ const win = isBrowser ? /** @type {Window & {AnimeJS: Array}} */ ( /** @type {unknown} */(window)) : null; /** @type {Document|null} */ 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 = /*#__PURE__*/ (() => { const map = new Map(); map.set('x', 'translateX'); map.set('y', 'translateY'); map.set('z', 'translateZ'); return map; })(); const validTransforms = [ 'translateX', 'translateY', 'translateZ', 'rotate', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'perspective', 'matrix', 'matrix3d', ]; const transformsFragmentStrings = /*#__PURE__*/ 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 scope = { /** @type {Scope} */ current: null, /** @type {Document|DOMTarget} */ root: doc, }; const globals = { /** @type {DefaultsParams} */ defaults, /** @type {Number} */ precision: 4, /** @type {Number} equals 1 in ms mode, 0.001 in s mode */ timeScale: 1, /** @type {Number} */ tickThreshold: 200, }; const globalVersions = { version: '4.1.3', 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; // 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} min * @param {Number} max * @param {Number} [decimalLength] * @return {Number} */ const random = (min, max, decimalLength) => { const m = 10 ** (decimalLength || 0); return floor((Math.random() * (max - min + (1 / m)) + min) * m) / m; }; /** * Adapted from https://bost.ocks.org/mike/shuffle/ * @param {Array} items * @return {Array} */ const shuffle = items => { let m = items.length, t, i; while (m) { i = random(0, --m); t = items[m]; items[m] = items[i]; items[i] = t; } return items; }; /** * @param {Number} v * @return {Number} */ const clampInfinity = v => v === Infinity ? maxValue : v === -Infinity ? -1e12 : v; /** * @param {Number} v * @return {Number} */ const normalizeTime = v => v <= minValue ? minValue : clampInfinity(round(v, 11)); // 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; }; /** * @param {(...args: any[]) => Tickable | ((...args: any[]) => void)} constructor * @return {(...args: any[]) => Tickable | ((...args: any[]) => void)} */ const createRefreshable = constructor => { /** @type {Tickable} */ let tracked; return (...args) => { let currentIteration, currentIterationProgress, reversed, alternate; if (tracked) { currentIteration = tracked.currentIteration; currentIterationProgress = tracked.iterationProgress; reversed = tracked.reversed; alternate = tracked._alternate; tracked.revert(); } const cleanup = constructor(...args); if (cleanup && !isFnc(cleanup) && cleanup.revert) tracked = cleanup; if (!isUnd(currentIterationProgress)) { /** @type {Tickable} */ (tracked).currentIteration = currentIteration; /** @type {Tickable} */ (tracked).iterationProgress = (alternate ? !(currentIteration % 2) ? reversed : !reversed : reversed) ? 1 - currentIterationProgress : currentIterationProgress; } return cleanup || noop; }; }; /* * 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 || tlIsRunningBackwards) && 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 = /*#__PURE__*/ (() => isBrowser ? requestAnimationFrame : setImmediate)(); const engineCancelMethod = /*#__PURE__*/ (() => 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; this.paused = true; /** @type {Number|NodeJS.Immediate} */ this.reqId = 0; } 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) { // Imediatly request a tick to update engine._elapsedTime and get accurate offsetPosition calculation in timer.js this.requestTick(now()); this.reqId = engineTickMethod(tickEngine); } return this; } pause() { if (!this.reqId) return; 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) ? scope.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 (!isBrowser) return /** @type {JSTargetsArray} */ (isArr(targets) && targets.flat(Infinity) || [targets]); 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; } 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] * @return {Number} */ const getScaleFactor = $el => { let scaleFactor = 1; if ($el && $el.getCTM) { const ctm = $el.getCTM(); if (ctm) { const scaleX = sqrt(ctm.a * ctm.a + ctm.b * ctm.b); const scaleY = sqrt(ctm.c * ctm.c + ctm.d * ctm.d); scaleFactor = (scaleX + scaleY) / 2; } } return scaleFactor; }; /** * Creates a proxy that wraps an SVGGeometryElement and adds drawing functionality. * @param {SVGGeometryElement} $el - The SVG element to transform into a drawable * @param {number} start - Starting position (0-1) * @param {number} end - Ending position (0-1) * @return {DrawableSVGGeometry} - Returns a proxy that preserves the original element's type with additional 'draw' attribute functionality */ const createDrawableProxy = ($el, start, end) => { const pathLength = K; const computedStyles = getComputedStyle($el); const strokeLineCap = computedStyles.strokeLinecap; // @ts-ignore const $scalled = computedStyles.vectorEffect === 'non-scaling-stroke' ? $el : null; let currentCap = strokeLineCap; const proxy = new Proxy($el, { get(target, property) { const value = target[property]; if (property === proxyTargetSymbol) return target; if (property === 'setAttribute') { 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 scaleFactor = getScaleFactor($scalled); const os = v1 * -1e3 * scaleFactor; const d1 = (v2 * pathLength * scaleFactor) + os; const d2 = (pathLength * scaleFactor + ((v1 === 0 && v2 === 1) || (v1 === 1 && v2 === 0) ? 0 : 10 * scaleFactor) - d1); if (strokeLineCap !== 'butt') { const newCap = v1 === v2 ? 'butt' : strokeLineCap; if (currentCap !== newCap) { target.style.strokeLinecap = `${newCap}`; currentCap = newCap; } } target.setAttribute('stroke-dashoffset', `${os}`); target.setAttribute('stroke-dasharray', `${d1} ${d2}`); } return Reflect.apply(value, target, args); }; } if (isFnc(value)) { return (...args) => Reflect.apply(value, target, args); } else { return value;