UNPKG

animejs

Version:

JavaScript animation engine

384 lines (330 loc) 13.1 kB
/** * 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;