animejs
Version:
JavaScript animation engine
326 lines (304 loc) • 10.6 kB
JavaScript
/**
* Anime.js - timeline - ESM
* @version v4.3.6
* @license MIT
* @copyright 2026 - Julian Garnier
*/
import { globals } from '../core/globals.js';
import { minValue, compositionTypes, tickModes } from '../core/consts.js';
import { isUnd, mergeObjects, isObj, isFnc, isStr, normalizeTime, forEachChildren, isNum, addChild, clampInfinity } from '../core/helpers.js';
import { setValue } from '../core/values.js';
import { parseTargets } from '../core/targets.js';
import { tick } from '../core/render.js';
import { cleanInlineStyles } from '../core/styles.js';
import { removeTargetsFromRenderable } from '../animation/composition.js';
import { JSAnimation } from '../animation/animation.js';
import { Timer } from '../timer/timer.js';
import { parseEase } from '../easings/eases/parser.js';
import { parseTimelinePosition } from './position.js';
/**
* @import {
* TargetsParam,
* Callback,
* Tickable,
* TimerParams,
* AnimationParams,
* Target,
* Renderable,
* TimelineParams,
* DefaultsParams,
* TimelinePosition,
* StaggerFunction,
* } from '../types/index.js'
*/
/**
* @import {
* WAAPIAnimation,
* } from '../waapi/waapi.js'
*/
/**
* @param {Timeline} tl
* @return {Number}
*/
function getTimelineTotalDuration(tl) {
return clampInfinity(((tl.iterationDuration + tl._loopDelay) * tl.iterationCount) - tl._loopDelay) || minValue;
}
/**
* @overload
* @param {TimerParams} childParams
* @param {Timeline} tl
* @param {Number} timePosition
* @return {Timeline}
*
* @overload
* @param {AnimationParams} childParams
* @param {Timeline} tl
* @param {Number} timePosition
* @param {TargetsParam} targets
* @param {Number} [index]
* @param {Number} [length]
* @return {Timeline}
*
* @param {TimerParams|AnimationParams} childParams
* @param {Timeline} tl
* @param {Number} timePosition
* @param {TargetsParam} [targets]
* @param {Number} [index]
* @param {Number} [length]
*/
function addTlChild(childParams, tl, timePosition, targets, index, length) {
const isSetter = isNum(childParams.duration) && /** @type {Number} */(childParams.duration) <= minValue;
// Offset the tl position with -minValue for 0 duration animations or .set() calls in order to align their end value with the defined position
const adjustedPosition = isSetter ? timePosition - minValue : timePosition;
if (tl.composition) tick(tl, adjustedPosition, 1, 1, tickModes.AUTO);
const tlChild = targets ?
new JSAnimation(targets,/** @type {AnimationParams} */(childParams), tl, adjustedPosition, false, index, length) :
new Timer(/** @type {TimerParams} */(childParams), tl, adjustedPosition);
if (tl.composition) tlChild.init(true);
// TODO: Might be better to insert at a position relative to startTime?
addChild(tl, tlChild);
forEachChildren(tl, (/** @type {Renderable} */child) => {
const childTLOffset = child._offset + child._delay;
const childDur = childTLOffset + child.duration;
if (childDur > tl.iterationDuration) tl.iterationDuration = childDur;
});
tl.duration = getTimelineTotalDuration(tl);
return tl;
}
let TLId = 0;
class Timeline extends Timer {
/**
* @param {TimelineParams} [parameters]
*/
constructor(parameters = {}) {
super(/** @type {TimerParams&TimelineParams} */(parameters), null, 0);
++TLId;
/** @type {String|Number} */
this.id = !isUnd(parameters.id) ? parameters.id : TLId;
/** @type {Number} */
this.duration = 0; // TL duration starts at 0 and grows when adding children
/** @type {Record<String, Number>} */
this.labels = {};
const defaultsParams = parameters.defaults;
const globalDefaults = globals.defaults;
/** @type {DefaultsParams} */
this.defaults = defaultsParams ? mergeObjects(defaultsParams, globalDefaults) : globalDefaults;
/** @type {Boolean} */
this.composition = setValue(parameters.composition, true);
/** @type {Callback<this>} */
this.onRender = parameters.onRender || globalDefaults.onRender;
const tlPlaybackEase = setValue(parameters.playbackEase, globalDefaults.playbackEase);
this._ease = tlPlaybackEase ? parseEase(tlPlaybackEase) : null;
/** @type {Number} */
this.iterationDuration = 0;
}
/**
* @overload
* @param {TargetsParam} a1
* @param {AnimationParams} a2
* @param {TimelinePosition|StaggerFunction<Number|String>} [a3]
* @return {this}
*
* @overload
* @param {TimerParams} a1
* @param {TimelinePosition} [a2]
* @return {this}
*
* @param {TargetsParam|TimerParams} a1
* @param {TimelinePosition|AnimationParams} a2
* @param {TimelinePosition|StaggerFunction<Number|String>} [a3]
*/
add(a1, a2, a3) {
const isAnim = isObj(a2);
const isTimer = isObj(a1);
if (isAnim || isTimer) {
this._hasChildren = true;
if (isAnim) {
const childParams = /** @type {AnimationParams} */(a2);
// Check for function for children stagger positions
if (isFnc(a3)) {
const staggeredPosition = a3;
const parsedTargetsArray = parseTargets(/** @type {TargetsParam} */(a1));
// Store initial duration before adding new children that will change the duration
const tlDuration = this.duration;
// Store initial _iterationDuration before adding new children that will change the duration
const tlIterationDuration = this.iterationDuration;
// Store the original id in order to add specific indexes to the new animations ids
const id = childParams.id;
let i = 0;
/** @type {Number} */
const parsedLength = (parsedTargetsArray.length);
parsedTargetsArray.forEach((/** @type {Target} */target) => {
// Create a new parameter object for each staggered children
const staggeredChildParams = { ...childParams };
// Reset the duration of the timeline iteration before each stagger to prevent wrong start value calculation
this.duration = tlDuration;
this.iterationDuration = tlIterationDuration;
if (!isUnd(id)) staggeredChildParams.id = id + '-' + i;
addTlChild(
staggeredChildParams,
this,
parseTimelinePosition(this, staggeredPosition(target, i, parsedLength, this)),
target,
i,
parsedLength
);
i++;
});
} else {
addTlChild(
childParams,
this,
parseTimelinePosition(this, a3),
/** @type {TargetsParam} */(a1),
);
}
} else {
// It's a Timer
addTlChild(
/** @type TimerParams */(a1),
this,
parseTimelinePosition(this,a2),
);
}
if (this.composition) this.init(true);
return this;
}
}
/**
* @overload
* @param {Tickable} [synced]
* @param {TimelinePosition} [position]
* @return {this}
*
* @overload
* @param {globalThis.Animation} [synced]
* @param {TimelinePosition} [position]
* @return {this}
*
* @overload
* @param {WAAPIAnimation} [synced]
* @param {TimelinePosition} [position]
* @return {this}
*
* @param {Tickable|WAAPIAnimation|globalThis.Animation} [synced]
* @param {TimelinePosition} [position]
*/
sync(synced, position) {
if (isUnd(synced) || synced && isUnd(synced.pause)) return this;
synced.pause();
const duration = +(/** @type {globalThis.Animation} */(synced).effect ? /** @type {globalThis.Animation} */(synced).effect.getTiming().duration : /** @type {Tickable} */(synced).duration);
// Forces WAAPI Animation to persist; otherwise, they will stop syncing on finish.
if (!isUnd(synced) && !isUnd(/** @type {WAAPIAnimation} */(synced).persist)) {
/** @type {WAAPIAnimation} */(synced).persist = true;
}
return this.add(synced, { currentTime: [0, duration], duration, delay: 0, ease: 'linear', playbackEase: 'linear' }, position);
}
/**
* @param {TargetsParam} targets
* @param {AnimationParams} parameters
* @param {TimelinePosition} [position]
* @return {this}
*/
set(targets, parameters, position) {
if (isUnd(parameters)) return this;
parameters.duration = minValue;
parameters.composition = compositionTypes.replace;
return this.add(targets, parameters, position);
}
/**
* @param {Callback<Timer>} callback
* @param {TimelinePosition} [position]
* @return {this}
*/
call(callback, position) {
if (isUnd(callback) || callback && !isFnc(callback)) return this;
return this.add({ duration: 0, delay: 0, onComplete: () => callback(this) }, position);
}
/**
* @param {String} labelName
* @param {TimelinePosition} [position]
* @return {this}
*
*/
label(labelName, position) {
if (isUnd(labelName) || labelName && !isStr(labelName)) return this;
this.labels[labelName] = parseTimelinePosition(this, position);
return this;
}
/**
* @param {TargetsParam} targets
* @param {String} [propertyName]
* @return {this}
*/
remove(targets, propertyName) {
removeTargetsFromRenderable(parseTargets(targets), this, propertyName);
return this;
}
/**
* @param {Number} newDuration
* @return {this}
*/
stretch(newDuration) {
const currentDuration = this.duration;
if (currentDuration === normalizeTime(newDuration)) return this;
const timeScale = newDuration / currentDuration;
const labels = this.labels;
forEachChildren(this, (/** @type {JSAnimation} */child) => child.stretch(child.duration * timeScale));
for (let labelName in labels) labels[labelName] *= timeScale;
return super.stretch(newDuration);
}
/**
* @return {this}
*/
refresh() {
forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => {
if (/** @type {JSAnimation} */(child).refresh) /** @type {JSAnimation} */(child).refresh();
});
return this;
}
/**
* @return {this}
*/
revert() {
super.revert();
forEachChildren(this, (/** @type {JSAnimation|Timer} */child) => child.revert, true);
return cleanInlineStyles(this);
}
/**
* @typedef {this & {then: null}} ResolvedTimeline
*/
/**
* @param {Callback<ResolvedTimeline>} [callback]
* @return Promise<this>
*/
then(callback) {
return super.then(callback);
}
}
/**
* @param {TimelineParams} [parameters]
* @return {Timeline}
*/
const createTimeline = parameters => new Timeline(parameters).init();
export { Timeline, createTimeline };