animejs
Version:
JavaScript animation engine
392 lines (344 loc) • 18.3 kB
JavaScript
/**
* Anime.js - core - CJS
* @version v4.3.6
* @license MIT
* @copyright 2026 - Julian Garnier
*/
'use strict';
var globals = require('./globals.cjs');
var consts = require('./consts.cjs');
var helpers = require('./helpers.cjs');
/**
* @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 = helpers.clamp(tickablePrevAbsoluteTime, -tickableDelay, duration);
const tickableCurrentTime = helpers.clamp(tickableAbsoluteTime, -tickableDelay, duration);
const deltaTime = tickableAbsoluteTime - tickablePrevAbsoluteTime;
const isCurrentTimeAboveZero = tickableCurrentTime > 0;
const isCurrentTimeEqualOrAboveDuration = tickableCurrentTime >= duration;
const isSetter = duration <= consts.minValue;
const forcedTick = tickMode === consts.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 = helpers.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 === consts.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.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 !== consts.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 = helpers.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 === consts.tweenTypes.OBJECT;
const tweenIsNumber = tweenValueType === consts.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.globals.precision;
// Recompose tween value
/** @type {String|Number} */
let value;
/** @type {Number} */
let number;
if (tweenIsNumber) {
value = number = /** @type {Number} */(tweenModifier(helpers.round(helpers.lerp(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision )));
} else if (tweenValueType === consts.valueTypes.UNIT) {
// Rounding the values speed up string composition
number = /** @type {Number} */(tweenModifier(helpers.round(helpers.lerp(tween._fromNumber, tween._toNumber, tweenProgress), tweenPrecision)));
value = `${number}${tween._unit}`;
} else if (tweenValueType === consts.valueTypes.COLOR) {
const fn = tween._fromNumbers;
const tn = tween._toNumbers;
const r = helpers.round(helpers.clamp(/** @type {Number} */(tweenModifier(helpers.lerp(fn[0], tn[0], tweenProgress))), 0, 255), 0);
const g = helpers.round(helpers.clamp(/** @type {Number} */(tweenModifier(helpers.lerp(fn[1], tn[1], tweenProgress))), 0, 255), 0);
const b = helpers.round(helpers.clamp(/** @type {Number} */(tweenModifier(helpers.lerp(fn[2], tn[2], tweenProgress))), 0, 255), 0);
const a = helpers.clamp(/** @type {Number} */(tweenModifier(helpers.round(helpers.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 === consts.valueTypes.COMPLEX) {
value = tween._strings[0];
for (let j = 0, l = tween._toNumbers.length; j < l; j++) {
const n = /** @type {Number} */(tweenModifier(helpers.round(helpers.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 !== consts.compositionTypes.blend) {
const tweenProperty = tween.property;
tweenTarget = tween.target;
if (tweenIsObject) {
tweenTarget[tweenProperty] = value;
} else if (tweenType === consts.tweenTypes.ATTRIBUTE) {
/** @type {DOMTarget} */(tweenTarget).setAttribute(tweenProperty, /** @type {String} */(value));
} else {
tweenStyle = /** @type {DOMTarget} */(tweenTarget).style;
if (tweenType === consts.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[consts.transformsSymbol];
}
tweenTargetTransformsProperties[tweenProperty] = value;
tweenTransformsNeedUpdate = 1;
} else if (tweenType === consts.tweenTypes.CSS) {
tweenStyle[tweenProperty] = value;
} else if (tweenType === consts.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 = consts.emptyString;
for (let key in tweenTargetTransformsProperties) {
str += `${consts.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 <= consts.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 = helpers.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;
helpers.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, consts.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 <= consts.minValue && (!childStartTime || childEndTime === tlIterationDuration)) {
child.onComplete(child);
}
}
});
if (!muteCallbacks) tl.onLoop(/** @type {CallbackArgument} */(tl));
}
helpers.forEachChildren(tl, (/** @type {JSAnimation} */child) => {
const childTime = helpers.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));
}
}
}
}
};
exports.render = render;
exports.tick = tick;