mortar.js

/**
 * The Mortar class will call the callback function at the given [frame rate]{@link https://en.wikipedia.org/wiki/Frame_rate}
 * defined by the <code>fps</code> argument (frames per second or times per second).
 * <br>
 * Mortar can be used to perform an animation through a {@link Mixer}, {@link Spice} or {@link Recipe} by calling its
 * <code>frame()</code> method as the callback function. The interpolations will be updated before the next repaint.
 * <br><br>
 * It tries to honors the given <code>fps</code> value, so consecutive calls to the callback function will run at the same
 * speed regardless on the performance of the device or the refresh rate of the screen. However, to ensure a consistent animation,
 * be sure to use the <code>delta</code> argument passed to the callback function to calculate how much the animation
 * will progress in a frame.
 * @example
import { Mixer, Spice, Mortar } from 'paprika-tween';
const spice = new Spice({
    duration: 5000,
    from: { scale: 1 },
    to: { scale: 2.5 },
    render: (props, interpolation) => { ... }
});
const mixer = new Mixer();
mixer.add(spice)
     .start();
const mortar = new Mortar((time) => mixer.frame(time));
mortar.start();
 * @since 1.0.0
 */
export class Mortar {
    /**
     * Creates a new instance of the Mortar object.
     * @param {function} [cb] - The function to be called at the given frame rate. It receives two arguments: the time since it started,
     * and the elapsed time since the previous frame (or delta time).
     * @param {Number} [fps=60] - The integer number of times to call the callback function per second (frames per second).
     * Number must be an integer.
     * @since 1.0.0
     */
    constructor(cb, fps = 60) {
        this._cb = cb;
        this._fpsInterval = 1000 / fps;
        this._running = false;
    }
    /**
     * Starts the frame-by-frame loop by internally calling to [requestAnimationFrame()]{@link https://developer.mozilla.org/en/docs/Web/API/Window/requestAnimationFrame}.
     * The callback function will be called the <code>fps</code> number of times per second.
     * @since 1.0.0
     */
    start() {
        this._startTime = performance.now();
        this._previousTime = this._startTime;
        this._previousDeltaTime = this._startTime;
        this._pausedTime = 0;
        this._running = true;
        // Wrapper to keep the scope (faster than .bind()?)
        this._onFrame = (currentTime) => this.frame(currentTime);
        this.frame(this._startTime);
    }
    /**
     * Pauses the frame-by-frame loop.
     * @since 1.0.0
     */
    pause() {
        this._running = false;
        this._previousPauseTime = performance.now();
    }
    /**
     * Resumes the frame-by-frame loop.
     * @since 1.0.0
     */
    resume() {
        const now = performance.now();
        this._pausedTime += now - this._previousPauseTime;
        this._running = true;
        this.frame(now);
    }
    /**
     * Stops the frame-by-frame loop and removes the callback function. Further calls to {@linkcode Mortar#start} will fail.
     * @since 1.0.0
     */
    stop() {
        this.pause();
        this._cb = null;
        this._onFrame = null;
    }
    /**
     * The <code>frame()</code> method is called before the browser performs the next repaint, then it calls the callback function.
     * Mortar will ensure that the callback function is called no more than <code>fps</code> number of times per second.
     * <br>
     * To keep the same speed in your animation, Be sure to use the <code>delta</code> argument to calculate how much the
     * animation will progress in a frame.
     * @since 1.0.0
     * @example
import { Mortar } from 'paprika-tween';
function loop(time, delta) {
    character.left += character.speed * delta;
}
const mortar = new Mortar(loop, 10);
mortar.start();
     */
    frame(currentTime) {
        if (!this._running) {
            return;
        }
        currentTime -= this._pausedTime;
        // Calculate elapsed time since last loop
        let delta = currentTime - this._previousDeltaTime;
        // If enough time has elapsed, draw the next frame
        if (delta >= this._fpsInterval) {
            // Adjust next execution time in case this frame took longer to execute
            this._previousDeltaTime = currentTime - (delta % this._fpsInterval);
            this._cb(currentTime, currentTime - this._previousTime);
            this._previousTime = currentTime;
        }
        if (this._onFrame) {
            requestAnimationFrame(this._onFrame);
        }
    }
    /**
     * Returns whether the instance is running or not.
     * @type {Boolean}
     */
    get running() {
        return this._running;
    }
    /**
     * Returns the time since the Mortar started.
     * @type {DOMHighResTimeStamp}
     */
    get time() {
        return this._previousTime;
    }
}