Source: player/Animator.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** @module */

import {EventEmitter} from "events";

/** The browser-specific function to request an animation frame.
 *
 * @readonly
 * @type {Function}
 */
const doRequestAnimationFrame =
        window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        window.oRequestAnimationFrame;

/** The object that provides the `now` method to read the current time.
 *
 * @readonly
 * @type {Function}
 */
const perf = window.performance && window.performance.now ? window.performance : Date;

/** The default time step.
 *
 * For browsers that do not support animation frames.
 *
 * @default
 * @type {number}
 */
const TIME_STEP_MS = 40;

/** The handle provided by `setInterval`.
 *
 * For browsers that do not support animation frames.
 */
let timer;

/** The number of running animators.
 *
 * @default
 * @type {number}
 */
let runningAnimators = 0;

/** The list of managed animators.
 *
 * @default
 * @type {module:player/Animator.Animator[]}
 */
const animatorList = [];

/** The main animation loop.
 *
 * This function is called periodically and triggers the
 * animation steps in all running animators.
 *
 * If all animators are removed from the list of running animators,
 * then the periodic calling is disabled.
 *
 * This function can be called either through `doRequestAnimationFrame`
 * or through `setInterval`.
 */
function loop() {
    if (runningAnimators > 0) {
        // If there is at least one animator,
        // and if the browser provides animation frames,
        // schedule this function to be called again in the next frame.
        if (doRequestAnimationFrame) {
            doRequestAnimationFrame(loop);
        }

        // Step all animators. We iterate over a copy of the animator list
        // in case the step() method removes an animator from the list.
        for (let animator of animatorList) {
            if (animator.running) {
                animator.step();
            }
        }
    }
    else if (!doRequestAnimationFrame) {
        // If all animators have been removed,
        // and if this function is called periodically
        // by setInterval(), disable the periodic calling.
        window.clearInterval(timer);
    }
}

/** Start the animation loop.
 *
 * This function delegates the periodic update of all animators
 * to the `loop` function, either using `doRequestAnimationFrame`
 * if the browser supports it, or using `setInterval`.
 */
function start() {
    if (doRequestAnimationFrame) {
        doRequestAnimationFrame(loop);
    }
    else {
        timer = window.setInterval(loop, TIME_STEP_MS);
    }
}

/** Fired by an animator on each animation step.
 *
 * @event module:player/Animator.step
 */

/** Fired by an animator when stopping an animation before completion.
 *
 * @event module:player/Animator.stop
 */

/** Fired by an animator when an animation is complete.
 *
 * @event module:player/Animator.done
 */

/** An animator provides the logic for animating other objects.
 *
 * The main purpose of an animator is to schedule the update
 * operations in the animated objects.
 *
 * @extends EventEmitter
 */
export class Animator extends EventEmitter {
    /** Initialize a new animator.
     *
     * The new animator is added to the list of animators
     * managed by the Sozi player.
     */
    constructor() {
        super();

        /** The duration of the current animation, in milliseconds.
         *
         * @default
         * @type {number} */
        this.durationMs = 500;

        /** The start time of the current animation.
         *
         * @default
         * @type {number} */
        this.initialTime = 0;

        /** The current running state of this animator.
         *
         * @default
         * @type {boolean} */
        this.running = false;

        animatorList.push(this);
    }

    /** Start a new animation.
     *
     * The {@linkcode module:player/Animator.step|step} event is fired once before starting the animation.
     *
     * @param {number} durationMs - The duration of the animation, in milliseconds.
     *
     * @fires module:player/Animator.step
     */
    start(durationMs) {
        this.durationMs = durationMs;
        this.initialTime = perf.now();
        this.emit("step", 0);
        if (!this.running) {
            this.running = true;
            runningAnimators ++;
            if (runningAnimators === 1) {
                start();
            }
        }
    }

    /** Stop the current animation.
     *
     * @fires module:player/Animator.step
     */
    stop() {
        if (this.running) {
            this.running = false;
            runningAnimators --;
            this.emit("stop");
        }
    }

    /** Perform one animation step.
     *
     * This function is called automatically by the main animation loop.
     * It fires the {@linkcode module:player/Animator.step|step} event with
     * an indication of the current progress (elapsed time / duration).
     *
     * If the animation duration has elapsed, the
     * {@linkcode module:player/Animator.done|done} event is fired.
     *
     * @fires module:player/Animator.step
     * @fires module:player/Animator.done
     */
    step() {
        const elapsedTime = perf.now() - this.initialTime;
        if (elapsedTime >= this.durationMs) {
            this.emit("step", 1);
            this.running = false;
            runningAnimators --;
            this.emit("done");
        } else {
            this.emit("step", elapsedTime / this.durationMs);
        }
    }
}