UNPKG

animejs

Version:

JavaScript animation engine

389 lines (342 loc) 18.1 kB
/** * Anime.js - core - ESM * @version v4.3.6 * @license MIT * @copyright 2026 - Julian Garnier */ import { globals } from './globals.js'; import { minValue, tickModes, valueTypes, compositionTypes, tweenTypes, transformsSymbol, transformsFragmentStrings, emptyString } from './consts.js'; import { forEachChildren, round, now, clamp, lerp } from './helpers.js'; /** * @import { * Tickable, * Renderable, * CallbackArgument, * Tween, * DOMTarget, * } from '../types/index.js' */ /** * @import { * JSAnimation, * } from '../animation/animation.js' */ /** * @import { * Timeline, * } from '../timeline/timeline.js' */ /** * @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(lerp(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision ))); } else if (tweenValueType === valueTypes.UNIT) { // Rounding the values speed up string composition number = /** @type {Number} */(tweenModifier(round(lerp(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(lerp(fn[0], tn[0], tweenProgress))), 0, 255), 0); const g = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[1], tn[1], tweenProgress))), 0, 255), 0); const b = round(clamp(/** @type {Number} */(tweenModifier(lerp(fn[2], tn[2], tweenProgress))), 0, 255), 0); const a = clamp(/** @type {Number} */(tweenModifier(round(lerp(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(lerp(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 && ( // (tickableAbsoluteTime > 0 instead) of (tickableAbsoluteTime >= duration) to prevent floating point precision issues // see: https://github.com/juliangarnier/anime/issues/1088 (parent.began && !isRunningBackwards && tickableAbsoluteTime > 0 && !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)); } } } } }; export { render, tick };