UNPKG

animejs

Version:

JavaScript animation engine

509 lines (453 loc) 16 kB
/** * 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;