animejs
Version:
JavaScript animation engine
384 lines (330 loc) • 13.1 kB
JavaScript
/**
* Anime.js - animation - CJS
* @version v4.3.6
* @license MIT
* @copyright 2026 - Julian Garnier
*/
'use strict';
var consts = require('../core/consts.cjs');
var helpers = require('../core/helpers.cjs');
var styles = require('../core/styles.cjs');
var engine = require('../engine/engine.cjs');
var additive = require('./additive.cjs');
/**
* @import {
* TweenReplaceLookups,
* TweenAdditiveLookups,
* TweenPropertySiblings,
* Tween,
* Target,
* TargetsArray,
* Renderable,
* } from '../types/index.js'
*
* @import {
* JSAnimation,
* } from '../animation/animation.js'
*/
const lookups = {
/** @type {TweenReplaceLookups} */
_rep: new WeakMap(),
/** @type {TweenAdditiveLookups} */
_add: new Map(),
};
/**
* @param {Target} target
* @param {String} property
* @param {String} lookup
* @return {TweenPropertySiblings}
*/
const getTweenSiblings = (target, property, lookup = '_rep') => {
const lookupMap = lookups[lookup];
let targetLookup = lookupMap.get(target);
if (!targetLookup) {
targetLookup = {};
lookupMap.set(target, targetLookup);
}
return targetLookup[property] ? targetLookup[property] : targetLookup[property] = {
_head: null,
_tail: null,
}
};
/**
* @param {Tween} p
* @param {Tween} c
* @return {Number|Boolean}
*/
const addTweenSortMethod = (p, c) => {
return p._isOverridden || p._absoluteStartTime > c._absoluteStartTime;
};
/**
* @param {Tween} tween
*/
const overrideTween = tween => {
tween._isOverlapped = 1;
tween._isOverridden = 1;
tween._changeDuration = consts.minValue;
tween._currentTime = consts.minValue;
};
/**
* @param {Tween} tween
* @param {TweenPropertySiblings} siblings
* @return {Tween}
*/
const composeTween = (tween, siblings) => {
const tweenCompositionType = tween._composition;
// Handle replaced tweens
if (tweenCompositionType === consts.compositionTypes.replace) {
const tweenAbsStartTime = tween._absoluteStartTime;
helpers.addChild(siblings, tween, addTweenSortMethod, '_prevRep', '_nextRep');
const prevSibling = tween._prevRep;
// Update the previous siblings for composition replace tweens
if (prevSibling) {
const prevParent = prevSibling.parent;
const prevAbsEndTime = prevSibling._absoluteStartTime + prevSibling._changeDuration;
// Handle looped animations tween
if (
// Check if the previous tween is from a different animation
tween.parent.id !== prevParent.id &&
// Check if the animation has loops
prevParent.iterationCount> 1 &&
// Check if _absoluteChangeEndTime of last loop overlaps the current tween
prevAbsEndTime + (prevParent.duration - prevParent.iterationDuration) > tweenAbsStartTime
) {
// TODO: Find a way to only override the iterations overlapping with the tween
overrideTween(prevSibling);
let prevPrevSibling = prevSibling._prevRep;
// If the tween was part of a set of keyframes, override its siblings
while (prevPrevSibling && prevPrevSibling.parent.id === prevParent.id) {
overrideTween(prevPrevSibling);
prevPrevSibling = prevPrevSibling._prevRep;
}
}
const absoluteUpdateStartTime = tweenAbsStartTime - tween._delay;
if (prevAbsEndTime > absoluteUpdateStartTime) {
const prevChangeStartTime = prevSibling._startTime;
const prevTLOffset = prevAbsEndTime - (prevChangeStartTime + prevSibling._updateDuration);
// Rounding is necessary here to minimize floating point errors when working in seconds
const updatedPrevChangeDuration = helpers.round(absoluteUpdateStartTime - prevTLOffset - prevChangeStartTime, 12);
prevSibling._changeDuration = updatedPrevChangeDuration;
prevSibling._currentTime = updatedPrevChangeDuration;
prevSibling._isOverlapped = 1;
// Override the previous tween if its new _changeDuration is lower than minValue
// TODO: See if it's even neceseeary to test against minValue, checking for 0 might be enough
if (updatedPrevChangeDuration < consts.minValue) {
overrideTween(prevSibling);
}
}
// Pause (and cancel) the parent if it only contains overlapped tweens
let pausePrevParentAnimation = true;
helpers.forEachChildren(prevParent, (/** @type Tween */t) => {
if (!t._isOverlapped) pausePrevParentAnimation = false;
});
if (pausePrevParentAnimation) {
const prevParentTL = prevParent.parent;
if (prevParentTL) {
let pausePrevParentTL = true;
helpers.forEachChildren(prevParentTL, (/** @type JSAnimation */a) => {
if (a !== prevParent) {
helpers.forEachChildren(a, (/** @type Tween */t) => {
if (!t._isOverlapped) pausePrevParentTL = false;
});
}
});
if (pausePrevParentTL) {
prevParentTL.cancel();
}
} else {
prevParent.cancel();
// Previously, calling .cancel() on a timeline child would affect the render order of other children
// Worked around this by marking it as .completed and using .pause() for safe removal in the engine loop
// This is no longer needed since timeline tween composition is now handled separately
// Keeping this here for reference
// prevParent.completed = true;
// prevParent.pause();
}
}
}
// let nextSibling = tween._nextRep;
// // All the next siblings are automatically overridden
// if (nextSibling && nextSibling._absoluteStartTime >= tweenAbsStartTime) {
// while (nextSibling) {
// overrideTween(nextSibling);
// nextSibling = nextSibling._nextRep;
// }
// }
// if (nextSibling && nextSibling._absoluteStartTime < tweenAbsStartTime) {
// while (nextSibling) {
// overrideTween(nextSibling);
// console.log(tween.id, nextSibling.id);
// nextSibling = nextSibling._nextRep;
// }
// }
// Handle additive tweens composition
} else if (tweenCompositionType === consts.compositionTypes.blend) {
const additiveTweenSiblings = getTweenSiblings(tween.target, tween.property, '_add');
const additiveAnimation = additive.addAdditiveAnimation(lookups._add);
let lookupTween = additiveTweenSiblings._head;
if (!lookupTween) {
lookupTween = { ...tween };
lookupTween._composition = consts.compositionTypes.replace;
lookupTween._updateDuration = consts.minValue;
lookupTween._startTime = 0;
lookupTween._numbers = helpers.cloneArray(tween._fromNumbers);
lookupTween._number = 0;
lookupTween._next = null;
lookupTween._prev = null;
helpers.addChild(additiveTweenSiblings, lookupTween);
helpers.addChild(additiveAnimation, lookupTween);
}
// Convert the values of TO to FROM and set TO to 0
const toNumber = tween._toNumber;
tween._fromNumber = lookupTween._fromNumber - toNumber;
tween._toNumber = 0;
tween._numbers = helpers.cloneArray(tween._fromNumbers);
tween._number = 0;
lookupTween._fromNumber = toNumber;
if (tween._toNumbers) {
const toNumbers = helpers.cloneArray(tween._toNumbers);
if (toNumbers) {
toNumbers.forEach((value, i) => {
tween._fromNumbers[i] = lookupTween._fromNumbers[i] - value;
tween._toNumbers[i] = 0;
});
}
lookupTween._fromNumbers = toNumbers;
}
helpers.addChild(additiveTweenSiblings, tween, null, '_prevAdd', '_nextAdd');
}
return tween;
};
/**
* @param {Tween} tween
* @return {Tween}
*/
const removeTweenSliblings = tween => {
const tweenComposition = tween._composition;
if (tweenComposition !== consts.compositionTypes.none) {
const tweenTarget = tween.target;
const tweenProperty = tween.property;
const replaceTweensLookup = lookups._rep;
const replaceTargetProps = replaceTweensLookup.get(tweenTarget);
const tweenReplaceSiblings = replaceTargetProps[tweenProperty];
helpers.removeChild(tweenReplaceSiblings, tween, '_prevRep', '_nextRep');
if (tweenComposition === consts.compositionTypes.blend) {
const addTweensLookup = lookups._add;
const addTargetProps = addTweensLookup.get(tweenTarget);
if (!addTargetProps) return;
const additiveTweenSiblings = addTargetProps[tweenProperty];
const additiveAnimation = additive.additive.animation;
helpers.removeChild(additiveTweenSiblings, tween, '_prevAdd', '_nextAdd');
// If only one tween is left in the additive lookup, it's the tween lookup
const lookupTween = additiveTweenSiblings._head;
if (lookupTween && lookupTween === additiveTweenSiblings._tail) {
helpers.removeChild(additiveTweenSiblings, lookupTween, '_prevAdd', '_nextAdd');
helpers.removeChild(additiveAnimation, lookupTween);
let shouldClean = true;
for (let prop in addTargetProps) {
if (addTargetProps[prop]._head) {
shouldClean = false;
break;
}
}
if (shouldClean) {
addTweensLookup.delete(tweenTarget);
}
}
}
}
return tween;
};
/**
* @param {TargetsArray} targetsArray
* @param {JSAnimation} animation
* @param {String} [propertyName]
* @return {Boolean}
*/
const removeTargetsFromJSAnimation = (targetsArray, animation, propertyName) => {
let tweensMatchesTargets = false;
helpers.forEachChildren(animation, (/**@type {Tween} */tween) => {
const tweenTarget = tween.target;
if (targetsArray.includes(tweenTarget)) {
const tweenName = tween.property;
const tweenType = tween._tweenType;
const normalizePropName = styles.sanitizePropertyName(propertyName, tweenTarget, tweenType);
if (!normalizePropName || normalizePropName && normalizePropName === tweenName) {
// Make sure to flag the previous CSS transform tween to renderTransform
if (tween.parent._tail === tween &&
tween._tweenType === consts.tweenTypes.TRANSFORM &&
tween._prev &&
tween._prev._tweenType === consts.tweenTypes.TRANSFORM
) {
tween._prev._renderTransforms = 1;
}
// Removes the tween from the selected animation
helpers.removeChild(animation, tween);
// Detach the tween from its siblings to make sure blended tweens are correctlly removed
removeTweenSliblings(tween);
tweensMatchesTargets = true;
}
}
}, true);
return tweensMatchesTargets;
};
/**
* @param {TargetsArray} targetsArray
* @param {Renderable} [renderable]
* @param {String} [propertyName]
*/
const removeTargetsFromRenderable = (targetsArray, renderable, propertyName) => {
const parent = /** @type {Renderable|typeof engine} **/(renderable ? renderable : engine.engine);
let removeMatches;
if (parent._hasChildren) {
let iterationDuration = 0;
helpers.forEachChildren(parent, (/** @type {Renderable} */child) => {
if (!child._hasChildren) {
removeMatches = removeTargetsFromJSAnimation(targetsArray, /** @type {JSAnimation} */(child), propertyName);
// Remove the child from its parent if no tweens and no children left after the removal
if (removeMatches && !child._head) {
child.cancel();
helpers.removeChild(parent, child);
} else {
// Calculate the new iterationDuration value to handle onComplete with last child in render()
const childTLOffset = child._offset + child._delay;
const childDur = childTLOffset + child.duration;
if (childDur > iterationDuration) {
iterationDuration = childDur;
}
}
}
// Make sure to also remove engine's children targets
// NOTE: Avoid recursion?
if (child._head) {
removeTargetsFromRenderable(targetsArray, child, propertyName);
} else {
child._hasChildren = false;
}
}, true);
// Update iterationDuration value to handle onComplete with last child in render()
if (!helpers.isUnd(/** @type {Renderable} */(parent).iterationDuration)) {
/** @type {Renderable} */(parent).iterationDuration = iterationDuration;
}
} else {
removeMatches = removeTargetsFromJSAnimation(
targetsArray,
/** @type {JSAnimation} */(parent),
propertyName
);
}
if (removeMatches && !parent._head) {
parent._hasChildren = false;
// Cancel the parent if there are no tweens and no children left after the removal
// We have to check if the .cancel() method exist to handle cases where the parent is the engine itself
if (/** @type {Renderable} */(parent).cancel) /** @type {Renderable} */(parent).cancel();
}
};
exports.composeTween = composeTween;
exports.getTweenSiblings = getTweenSiblings;
exports.overrideTween = overrideTween;
exports.removeTargetsFromRenderable = removeTargetsFromRenderable;
exports.removeTweenSliblings = removeTweenSliblings;