animejs
Version:
JavaScript animation engine
509 lines (453 loc) • 16 kB
JavaScript
/**
* Anime.js - timer - 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 globals = require('../core/globals.cjs');
var values = require('../core/values.cjs');
var render = require('../core/render.cjs');
var composition = require('../animation/composition.cjs');
var clock = require('../core/clock.cjs');
var engine = require('../engine/engine.cjs');
/**
* @import {
* Callback,
* TimerParams,
* Renderable,
* Tween,
* } from '../types/index.js'
*/
/**
* @import {
* ScrollObserver,
* } from '../events/scroll.js'
*/
/**
* @import {
* Timeline,
* } from '../timeline/timeline.js'
*/
/**
* @param {Timer} timer
* @return {Timer}
*/
const resetTimerProperties = timer => {
timer.paused = true;
timer.began = false;
timer.completed = false;
return timer;
};
/**
* @param {Timer} timer
* @return {Timer}
*/
const reviveTimer = timer => {
if (!timer._cancelled) return timer;
if (timer._hasChildren) {
helpers.forEachChildren(timer, reviveTimer);
} else {
helpers.forEachChildren(timer, (/** @type {Tween} tween */tween) => {
if (tween._composition !== consts.compositionTypes.none) {
composition.composeTween(tween, composition.getTweenSiblings(tween.target, tween.property));
}
});
}
timer._cancelled = 0;
return timer;
};
let timerId = 0;
/**
* Base class used to create Timers, Animations and Timelines
*/
class Timer extends clock.Clock {
/**
* @param {TimerParams} [parameters]
* @param {Timeline} [parent]
* @param {Number} [parentPosition]
*/
constructor(parameters = {}, parent = null, parentPosition = 0) {
super(0);
++timerId;
const {
id,
delay,
duration,
reversed,
alternate,
loop,
loopDelay,
autoplay,
frameRate,
playbackRate,
onComplete,
onLoop,
onPause,
onBegin,
onBeforeUpdate,
onUpdate,
} = parameters;
if (globals.scope.current) globals.scope.current.register(this);
const timerInitTime = parent ? 0 : engine.engine._lastTickTime;
const timerDefaults = parent ? parent.defaults : globals.globals.defaults;
const timerDelay = /** @type {Number} */(helpers.isFnc(delay) || helpers.isUnd(delay) ? timerDefaults.delay : +delay);
const timerDuration = helpers.isFnc(duration) || helpers.isUnd(duration) ? Infinity : +duration;
const timerLoop = values.setValue(loop, timerDefaults.loop);
const timerLoopDelay = values.setValue(loopDelay, timerDefaults.loopDelay);
let timerIterationCount = timerLoop === true ||
timerLoop === Infinity ||
/** @type {Number} */(timerLoop) < 0 ? Infinity :
/** @type {Number} */(timerLoop) + 1;
if (globals.devTools) {
const isInfinite = timerIterationCount === Infinity;
const registered = globals.devTools.register(this, parameters, isInfinite);
if (registered && isInfinite) {
const minIterations = alternate ? 2 : 1;
const iterations = parent ? globals.devTools.maxNestedInfiniteLoops : globals.devTools.maxInfiniteLoops;
timerIterationCount = Math.max(iterations, minIterations);
}
}
let offsetPosition = 0;
if (parent) {
offsetPosition = parentPosition;
} else {
// Make sure to tick the engine once if not currently running to get up to date engine._lastTickTime
// to avoid big gaps with the following offsetPosition calculation
if (!engine.engine.reqId) engine.engine.requestTick(helpers.now());
// Make sure to scale the offset position with globals.timeScale to properly handle seconds unit
offsetPosition = (engine.engine._lastTickTime - engine.engine._startTime) * globals.globals.timeScale;
}
// Timer's parameters
/** @type {String|Number} */
this.id = !helpers.isUnd(id) ? id : timerId;
/** @type {Timeline} */
this.parent = parent;
// Total duration of the timer
this.duration = helpers.clampInfinity(((timerDuration + timerLoopDelay) * timerIterationCount) - timerLoopDelay) || consts.minValue;
/** @type {Boolean} */
this.backwards = false;
/** @type {Boolean} */
this.paused = true;
/** @type {Boolean} */
this.began = false;
/** @type {Boolean} */
this.completed = false;
/** @type {Callback<this>} */
this.onBegin = onBegin || timerDefaults.onBegin;
/** @type {Callback<this>} */
this.onBeforeUpdate = onBeforeUpdate || timerDefaults.onBeforeUpdate;
/** @type {Callback<this>} */
this.onUpdate = onUpdate || timerDefaults.onUpdate;
/** @type {Callback<this>} */
this.onLoop = onLoop || timerDefaults.onLoop;
/** @type {Callback<this>} */
this.onPause = onPause || timerDefaults.onPause;
/** @type {Callback<this>} */
this.onComplete = onComplete || timerDefaults.onComplete;
/** @type {Number} */
this.iterationDuration = timerDuration; // Duration of one loop
/** @type {Number} */
this.iterationCount = timerIterationCount; // Number of loops
/** @type {Boolean|ScrollObserver} */
this._autoplay = parent ? false : values.setValue(autoplay, timerDefaults.autoplay);
/** @type {Number} */
this._offset = offsetPosition;
/** @type {Number} */
this._delay = timerDelay;
/** @type {Number} */
this._loopDelay = timerLoopDelay;
/** @type {Number} */
this._iterationTime = 0;
/** @type {Number} */
this._currentIteration = 0; // Current loop index
/** @type {Function} */
this._resolve = consts.noop; // Used by .then()
/** @type {Boolean} */
this._running = false;
/** @type {Number} */
this._reversed = +values.setValue(reversed, timerDefaults.reversed);
/** @type {Number} */
this._reverse = this._reversed;
/** @type {Number} */
this._cancelled = 0;
/** @type {Boolean} */
this._alternate = values.setValue(alternate, timerDefaults.alternate);
/** @type {Renderable} */
this._prev = null;
/** @type {Renderable} */
this._next = null;
// Clock's parameters
/** @type {Number} */
this._lastTickTime = timerInitTime;
/** @type {Number} */
this._startTime = timerInitTime;
/** @type {Number} */
this._lastTime = timerInitTime;
/** @type {Number} */
this._fps = values.setValue(frameRate, timerDefaults.frameRate);
/** @type {Number} */
this._speed = values.setValue(playbackRate, timerDefaults.playbackRate);
}
get cancelled() {
return !!this._cancelled;
}
set cancelled(cancelled) {
cancelled ? this.cancel() : this.reset(true).play();
}
get currentTime() {
return helpers.clamp(helpers.round(this._currentTime, globals.globals.precision), -this._delay, this.duration);
}
set currentTime(time) {
const paused = this.paused;
// Pausing the timer is necessary to avoid time jumps on a running instance
this.pause().seek(+time);
if (!paused) this.resume();
}
get iterationCurrentTime() {
return helpers.clamp(helpers.round(this._iterationTime, globals.globals.precision), 0, this.iterationDuration);
}
set iterationCurrentTime(time) {
this.currentTime = (this.iterationDuration * this._currentIteration) + time;
}
get progress() {
return helpers.clamp(helpers.round(this._currentTime / this.duration, 10), 0, 1);
}
set progress(progress) {
this.currentTime = this.duration * progress;
}
get iterationProgress() {
return helpers.clamp(helpers.round(this._iterationTime / this.iterationDuration, 10), 0, 1);
}
set iterationProgress(progress) {
const iterationDuration = this.iterationDuration;
this.currentTime = (iterationDuration * this._currentIteration) + (iterationDuration * progress);
}
get currentIteration() {
return this._currentIteration;
}
set currentIteration(iterationCount) {
this.currentTime = (this.iterationDuration * helpers.clamp(+iterationCount, 0, this.iterationCount - 1));
}
get reversed() {
return !!this._reversed;
}
set reversed(reverse) {
reverse ? this.reverse() : this.play();
}
get speed() {
return super.speed;
}
set speed(playbackRate) {
super.speed = playbackRate;
this.resetTime();
}
/**
* @param {Boolean} [softReset]
* @return {this}
*/
reset(softReset = false) {
// If cancelled, revive the timer before rendering in order to have propertly composed tweens siblings
reviveTimer(this);
if (this._reversed && !this._reverse) this.reversed = false;
// Rendering before updating the completed flag to prevent skips and to make sure the properties are not overridden
// Setting the iterationTime at the end to force the rendering to happend backwards, otherwise calling .reset() on Timelines might not render children in the right order
// NOTE: This is only required for Timelines and might be better to move to the Timeline class?
this._iterationTime = this.iterationDuration;
// Set tickMode to tickModes.FORCE to force rendering
render.tick(this, 0, 1, ~~softReset, consts.tickModes.FORCE);
// Reset timer properties after revive / render to make sure the props are not updated again
resetTimerProperties(this);
// Also reset children properties
if (this._hasChildren) {
helpers.forEachChildren(this, resetTimerProperties);
}
return this;
}
/**
* @param {Boolean} internalRender
* @return {this}
*/
init(internalRender = false) {
this.fps = this._fps;
this.speed = this._speed;
// Manually calling .init() on timelines should render all children intial state
// Forces all children to render once then render to 0 when reseted
if (!internalRender && this._hasChildren) {
render.tick(this, this.duration, 1, ~~internalRender, consts.tickModes.FORCE);
}
this.reset(internalRender);
// Make sure to set autoplay to false to child timers so it doesn't attempt to autoplay / link
const autoplay = this._autoplay;
if (autoplay === true) {
this.resume();
} else if (autoplay && !helpers.isUnd(/** @type {ScrollObserver} */(autoplay).linked)) {
/** @type {ScrollObserver} */(autoplay).link(this);
}
return this;
}
/** @return {this} */
resetTime() {
const timeScale = 1 / (this._speed * engine.engine._speed);
// TODO: See if we can safely use engine._lastTickTime here
// if (!engine.reqId) engine.requestTick(now())
// this._startTime = engine._lastTickTime - (this._currentTime + this._delay) * timeScale;
this._startTime = helpers.now() - (this._currentTime + this._delay) * timeScale;
return this;
}
/** @return {this} */
pause() {
if (this.paused) return this;
this.paused = true;
this.onPause(this);
return this;
}
/** @return {this} */
resume() {
if (!this.paused) return this;
this.paused = false;
// We can safely imediatly render a timer that has no duration and no children
if (this.duration <= consts.minValue && !this._hasChildren) {
render.tick(this, consts.minValue, 0, 0, consts.tickModes.FORCE);
} else {
if (!this._running) {
helpers.addChild(engine.engine, this);
engine.engine._hasChildren = true;
this._running = true;
}
this.resetTime();
// Forces the timer to advance by at least one frame when the next tick occurs
this._startTime -= 12;
engine.engine.wake();
}
return this;
}
/** @return {this} */
restart() {
return this.reset().resume();
}
/**
* @param {Number} time
* @param {Boolean|Number} [muteCallbacks]
* @param {Boolean|Number} [internalRender]
* @return {this}
*/
seek(time, muteCallbacks = 0, internalRender = 0) {
// Recompose the tween siblings in case the timer has been cancelled
reviveTimer(this);
// If you seek a completed animation, otherwise the next play will starts at 0
this.completed = false;
const isPaused = this.paused;
this.paused = true;
// timer, time, muteCallbacks, internalRender, tickMode
render.tick(this, time + this._delay, ~~muteCallbacks, ~~internalRender, consts.tickModes.AUTO);
return isPaused ? this : this.resume();
}
/** @return {this} */
alternate() {
const reversed = this._reversed;
const count = this.iterationCount;
const duration = this.iterationDuration;
// Calculate the maximum iterations possible given the iteration duration
const iterations = count === Infinity ? helpers.floor(consts.maxValue / duration) : count;
this._reversed = +(this._alternate && !(iterations % 2) ? reversed : !reversed);
if (count === Infinity) {
// Handle infinite loops to loop on themself
this.iterationProgress = this._reversed ? 1 - this.iterationProgress : this.iterationProgress;
} else {
this.seek((duration * iterations) - this._currentTime);
}
this.resetTime();
return this;
}
/** @return {this} */
play() {
if (this._reversed) this.alternate();
return this.resume();
}
/** @return {this} */
reverse() {
if (!this._reversed) this.alternate();
return this.resume();
}
// TODO: Move all the animation / tweens / children related code to Animation / Timeline
/** @return {this} */
cancel() {
if (this._hasChildren) {
helpers.forEachChildren(this, (/** @type {Renderable} */child) => child.cancel(), true);
} else {
helpers.forEachChildren(this, composition.removeTweenSliblings);
}
this._cancelled = 1;
// Pausing the timer removes it from the engine
return this.pause();
}
/**
* @param {Number} newDuration
* @return {this}
*/
stretch(newDuration) {
const currentDuration = this.duration;
const normlizedDuration = helpers.normalizeTime(newDuration);
if (currentDuration === normlizedDuration) return this;
const timeScale = newDuration / currentDuration;
const isSetter = newDuration <= consts.minValue;
this.duration = isSetter ? consts.minValue : normlizedDuration;
this.iterationDuration = isSetter ? consts.minValue : helpers.normalizeTime(this.iterationDuration * timeScale);
this._offset *= timeScale;
this._delay *= timeScale;
this._loopDelay *= timeScale;
return this;
}
/**
* Cancels the timer by seeking it back to 0 and reverting the attached scroller if necessary
* @return {this}
*/
revert() {
render.tick(this, 0, 1, 0, consts.tickModes.AUTO);
const ap = /** @type {ScrollObserver} */(this._autoplay);
if (ap && ap.linked && ap.linked === this) ap.revert();
return this.cancel();
}
/**
* Imediatly completes the timer, cancels it and triggers the onComplete callback
* @param {Boolean|Number} [muteCallbacks]
* @return {this}
*/
complete(muteCallbacks = 0) {
return this.seek(this.duration, muteCallbacks).cancel();
}
/**
* @typedef {this & {then: null}} ResolvedTimer
*/
/**
* @param {Callback<ResolvedTimer>} [callback]
* @return Promise<this>
*/
then(callback = consts.noop) {
const then = this.then;
const onResolve = () => {
// this.then = null prevents infinite recursion if returned by an async function
// https://github.com/juliangarnierorg/anime-beta/issues/26
this.then = null;
callback(/** @type {ResolvedTimer} */(this));
this.then = then;
this._resolve = consts.noop;
};
return new Promise(r => {
this._resolve = () => r(onResolve());
// Make sure to resolve imediatly if the timer has already completed
if (this.completed) this._resolve();
return this;
});
}
}
/**
* @param {TimerParams} [parameters]
* @return {Timer}
*/
const createTimer = parameters => new Timer(parameters, null, 0).init();
exports.Timer = Timer;
exports.createTimer = createTimer;