UNPKG

animejs

Version:

JavaScript animation engine

1,514 lines (1,321 loc) 258 kB
/** * anime.js - ESM * @version v4.0.0 * @author Julian Garnier * @license MIT * @copyright (c) 2025 Julian Garnier * @see https://animejs.com */ /** * @typedef {Object} DefaultsParams * @property {number|string} [id] * @property {PercentageKeyframes|DurationKeyframes} [keyframes] * @property {EasingParam} [playbackEase] * @property {number} [playbackRate] * @property {number} [frameRate] * @property {number|boolean} [loop] * @property {boolean} [reversed] * @property {boolean} [alternate] * @property {boolean|ScrollObserver} [autoplay] * @property {number|FunctionValue} [duration] * @property {number|FunctionValue} [delay] * @property {number} [loopDelay] * @property {EasingParam} [ease] * @property {'none'|'replace'|'blend'|compositionTypes} [composition] * @property {(v: any) => any} [modifier] * @property {(tickable: Tickable) => void} [onBegin] * @property {(tickable: Tickable) => void} [onBeforeUpdate] * @property {(tickable: Tickable) => void} [onUpdate] * @property {(tickable: Tickable) => void} [onLoop] * @property {(tickable: Tickable) => void} [onPause] * @property {(tickable: Tickable) => void} [onComplete] * @property {(renderable: Renderable) => void} [onRender] */ /** @typedef {JSAnimation|Timeline} Renderable */ /** @typedef {Timer|Renderable} Tickable */ /** @typedef {Timer&JSAnimation&Timeline} CallbackArgument */ /** @typedef {Animatable|Tickable|Draggable|ScrollObserver|Scope} Revertible */ /** * @callback EasingFunction * @param {Number} time * @return {Number} */ /** * @typedef {('linear'|'linear(x1, x2 25%, x3)'|'in'|'out'|'inOut'|'inQuad'|'outQuad'|'inOutQuad'|'inCubic'|'outCubic'|'inOutCubic'|'inQuart'|'outQuart'|'inOutQuart'|'inQuint'|'outQuint'|'inOutQuint'|'inSine'|'outSine'|'inOutSine'|'inCirc'|'outCirc'|'inOutCirc'|'inExpo'|'outExpo'|'inOutExpo'|'inBounce'|'outBounce'|'inOutBounce'|'inBack'|'outBack'|'inOutBack'|'inElastic'|'outElastic'|'inOutElastic'|'irregular'|'cubicBezier'|'steps'|'in(p = 1.675)'|'out(p = 1.675)'|'inOut(p = 1.675)'|'inBack(overshoot = 1.70158)'|'outBack(overshoot = 1.70158)'|'inOutBack(overshoot = 1.70158)'|'inElastic(amplitude = 1, period = .3)'|'outElastic(amplitude = 1, period = .3)'|'inOutElastic(amplitude = 1, period = .3)'|'irregular(length = 10, randomness = 1)'|'cubicBezier(x1, y1, x2, y2)'|'steps(steps = 10)')} EaseStringParamNames */ // A hack to get both ease names suggestions AND allow any strings // https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421 /** @typedef {(String & {})|EaseStringParamNames|EasingFunction|Spring} EasingParam */ /** @typedef {HTMLElement|SVGElement} DOMTarget */ /** @typedef {Record<String, any>} JSTarget */ /** @typedef {DOMTarget|JSTarget} Target */ /** @typedef {Target|NodeList|String} TargetSelector */ /** @typedef {DOMTarget|NodeList|String} DOMTargetSelector */ /** @typedef {Array.<DOMTargetSelector>|DOMTargetSelector} DOMTargetsParam */ /** @typedef {Array.<DOMTarget>} DOMTargetsArray */ /** @typedef {Array.<JSTarget>|JSTarget} JSTargetsParam */ /** @typedef {Array.<JSTarget>} JSTargetsArray */ /** @typedef {Array.<TargetSelector>|TargetSelector} TargetsParam */ /** @typedef {Array.<Target>} TargetsArray */ /** * @callback FunctionValue * @param {Target} target - The animated target * @param {Number} index - The target index * @param {Number} length - The total number of animated targets * @return {Number|String|TweenObjectValue|Array.<Number|String|TweenObjectValue>} */ /** * @callback TweenModifier * @param {Number} value - The animated value * @return {Number|String} */ /** @typedef {[Number, Number, Number, Number]} ColorArray */ /** * @template T * @callback Callback * @param {T} self - Returns itself * @param {PointerEvent} [e] * @return {*} */ /** * @template {object} T * @typedef {Object} TickableCallbacks * @property {Callback<T>} [onBegin] * @property {Callback<T>} [onBeforeUpdate] * @property {Callback<T>} [onUpdate] * @property {Callback<T>} [onLoop] * @property {Callback<T>} [onPause] * @property {Callback<T>} [onComplete] */ /** * @template {object} T * @typedef {Object} RenderableCallbacks * @property {Callback<T>} [onRender] */ /** * @typedef {Object} Tween * @property {Number} id * @property {JSAnimation} parent * @property {String} property * @property {Target} target * @property {String|Number} _value * @property {Function|null} _func * @property {EasingFunction} _ease * @property {Array.<Number>} _fromNumbers * @property {Array.<Number>} _toNumbers * @property {Array.<String>} _strings * @property {Number} _fromNumber * @property {Number} _toNumber * @property {Array.<Number>} _numbers * @property {Number} _number * @property {String} _unit * @property {TweenModifier} _modifier * @property {Number} _currentTime * @property {Number} _delay * @property {Number} _updateDuration * @property {Number} _startTime * @property {Number} _changeDuration * @property {Number} _absoluteStartTime * @property {tweenTypes} _tweenType * @property {valueTypes} _valueType * @property {Number} _composition * @property {Number} _isOverlapped * @property {Number} _isOverridden * @property {Number} _renderTransforms * @property {Tween} _prevRep * @property {Tween} _nextRep * @property {Tween} _prevAdd * @property {Tween} _nextAdd * @property {Tween} _prev * @property {Tween} _next */ /** * @typedef TweenDecomposedValue * @property {Number} t - Type * @property {Number} n - Single number value * @property {String} u - Value unit * @property {String} o - Value operator * @property {Array.<Number>} d - Array of Numbers (in case of complex value type) * @property {Array.<String>} s - Strings (in case of complex value type) */ /** @typedef {{_head: null|Tween, _tail: null|Tween}} TweenPropertySiblings */ /** @typedef {Record<String, TweenPropertySiblings>} TweenLookups */ /** @typedef {WeakMap.<Target, TweenLookups>} TweenReplaceLookups */ /** @typedef {Map.<Target, TweenLookups>} TweenAdditiveLookups */ /** * @typedef {Object} TimerOptions * @property {Number|String} [id] * @property {TweenParamValue} [duration] * @property {TweenParamValue} [delay] * @property {Number} [loopDelay] * @property {Boolean} [reversed] * @property {Boolean} [alternate] * @property {Boolean|Number} [loop] * @property {Boolean|ScrollObserver} [autoplay] * @property {Number} [frameRate] * @property {Number} [playbackRate] */ /** /** * @typedef {TimerOptions & TickableCallbacks<Timer>} TimerParams */ /** * @typedef {Number|String|FunctionValue} TweenParamValue */ /** * @typedef {TweenParamValue|[TweenParamValue, TweenParamValue]} TweenPropValue */ /** * @typedef {(String & {})|'none'|'replace'|'blend'|compositionTypes} TweenComposition */ /** * @typedef {Object} TweenParamsOptions * @property {TweenParamValue} [duration] * @property {TweenParamValue} [delay] * @property {EasingParam} [ease] * @property {TweenModifier} [modifier] * @property {TweenComposition} [composition] */ /** * @typedef {Object} TweenValues * @property {TweenParamValue} [from] * @property {TweenPropValue} [to] * @property {TweenPropValue} [fromTo] */ /** * @typedef {TweenParamsOptions & TweenValues} TweenKeyValue */ /** * @typedef {Array.<TweenKeyValue|TweenPropValue>} ArraySyntaxValue */ /** * @typedef {TweenParamValue|ArraySyntaxValue|TweenKeyValue} TweenOptions */ /** * @typedef {Partial<{to: TweenParamValue|Array.<TweenParamValue>; from: TweenParamValue|Array.<TweenParamValue>; fromTo: TweenParamValue|Array.<TweenParamValue>;}>} TweenObjectValue */ /** * @typedef {Object} PercentageKeyframeOptions * @property {EasingParam} [ease] */ /** * @typedef {Record<String, TweenParamValue>} PercentageKeyframeParams */ /** * @typedef {Record<String, PercentageKeyframeParams & PercentageKeyframeOptions>} PercentageKeyframes */ /** * @typedef {Array<Record<String, TweenOptions | TweenModifier | boolean> & TweenParamsOptions>} DurationKeyframes */ /** * @typedef {Object} AnimationOptions * @property {PercentageKeyframes|DurationKeyframes} [keyframes] * @property {EasingParam} [playbackEase] */ // TODO: Currently setting TweenModifier to the intersected Record<> makes the FunctionValue type target param any if only one parameter is set /** * @typedef {Record<String, TweenOptions | Callback<JSAnimation> | TweenModifier | boolean | PercentageKeyframes | DurationKeyframes | ScrollObserver> & TimerOptions & AnimationOptions & TweenParamsOptions & TickableCallbacks<JSAnimation> & RenderableCallbacks<JSAnimation>} AnimationParams */ /** * @typedef {Object} TimelineOptions * @property {DefaultsParams} [defaults] * @property {EasingParam} [playbackEase] */ /** * @typedef {TimerOptions & TimelineOptions & TickableCallbacks<Timeline> & RenderableCallbacks<Timeline>} TimelineParams */ /** * @callback AnimatablePropertySetter * @param {Number|Array.<Number>} to * @param {Number} [duration] * @param {EasingParam} [ease] * @return {AnimatableObject} */ /** * @callback AnimatablePropertyGetter * @return {Number|Array.<Number>} */ /** * @typedef {AnimatablePropertySetter & AnimatablePropertyGetter} AnimatableProperty */ /** * @typedef {Animatable & Record<String, AnimatableProperty>} AnimatableObject */ /** * @typedef {Object} AnimatablePropertyParamsOptions * @property {String} [unit] * @property {TweenParamValue} [duration] * @property {EasingParam} [ease] * @property {TweenModifier} [modifier] * @property {TweenComposition} [composition] */ /** * @typedef {Record<String, TweenParamValue | EasingParam | TweenModifier | TweenComposition | AnimatablePropertyParamsOptions> & AnimatablePropertyParamsOptions} AnimatableParams */ // 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 pathLe