Source: system/Time.js

//------------------------------------------------------------------------------
// Constructor scope
//------------------------------------------------------------------------------

/**
 * Creates a new instance of the Time class.
 *
 * @constructor
 *
 * @param {number} [framerate=60] Target framerate.
 * 
 * @class
 * @classdesc
 * 
 * The Time class is used to calculate the elapsed time within an application. 
 * Calculated time is used to run the application's update and rendering loop.
 */
rune.system.Time = function(framerate) {
    
    //--------------------------------------------------------------------------
    // Private properties
    //--------------------------------------------------------------------------
    
    /**
     * Buffer for elapsed time between fixed time steps.
     *
     * @type {number}
     * @private
     */
    this.m_buffer = 0;
    
    /**
     * Timestamp for the current point in time, ie the current frame.
     *
     * @type {number}
     * @private
     */
    this.m_currentTime = 0;
    
    /**
     * Current frame rate, ie an indication of whether the application is 
     * executed at the requested frame rate. If the application is executed 
     * correctly, the current frame rate should be the same as the requested 
     * frame rate.
     *
     * @type {number}
     * @private
     */
    this.m_currentFramerate = 0;
    
    /**
     * Timestamp from previous frame (tick).
     *
     * @type {number}
     * @private
     */
    this.m_previousTime = 0;
    
    /**
     * Represents the rendering stack, ie a list of methods that aim to draw 
     * the application's graphics to the screen for each active frame.
     *
     * @type {rune.util.Stack}
     * @private
     */
    this.m_render = new rune.util.Stack();
    
    /**
     * Describes the theoretical length of a frame in milliseconds.
     *
     * @type {number}
     * @private
     */
    this.m_step = 1000 / (framerate || 60);
    
    /**
     * List of timestamps.
     *
     * @type {Array}
     * @private
     */
    this.m_ticks = [];
    
    /**
     * Request ID from latest requestAnimationFrame call.
     *
     * @type {number}
     * @private
     */
    this.m_timeLoopID = 0;
    
    /**
     * Represents the update stack, ie a list of methods that aim to perform 
     * the necessary calculations for each frame. This stack is always executed 
     * before the rendering stack.
     *
     * @type {rune.util.Stack}
     * @private
     */
    this.m_update = new rune.util.Stack();
}

//------------------------------------------------------------------------------
// Public prototype getter and setter methods
//------------------------------------------------------------------------------

/**
 * Current frame rate, ie an indication of whether the application is 
 * executed at the requested frame rate. If the application is executed 
 * correctly, the current frame rate should be the same as the requested 
 * frame rate.
 *
 * @member {number} currentFramerate
 * @memberof rune.system.Time
 * @instance
 * @readonly
 */
Object.defineProperty(rune.system.Time.prototype, "currentFramerate", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return rune.util.Math.clamp(this.m_currentFramerate, 0, this['targetFramerate']);
    },
});

/**
 * Current frame rate divided by target frame rate.
 *
 * @member {number} quotient
 * @memberof rune.system.Time
 * @instance
 * @readonly
 */
Object.defineProperty(rune.system.Time.prototype, "quotient", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return this['currentFramerate'] / this['targetFramerate'];
    },
});

/**
 * Represents the rendering stack, ie a list of methods that aim to draw 
 * the application's graphics to the screen for each active frame.
 *
 * @member {rune.util.Stack} render
 * @memberof rune.system.Time
 * @instance
 * @readonly
 */
Object.defineProperty(rune.system.Time.prototype, "render", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return this.m_render;
    }
});

/**
 * Time scale relative to the system's maximum capacity. An application can 
 * calculate up to 60 ticks per second, this corresponds to scale 1.0. If an 
 * application is executed at 30 ticks per second, the time scale is 2.0. This 
 * information is important if you want to create an application that is 
 * executed at the same speed regardless of the application's frame rate.
 *
 * @member {number} scale
 * @memberof rune.system.Time
 * @instance
 * @readonly
 */
Object.defineProperty(rune.system.Time.prototype, "scale", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return this.m_step / ((1 / 60) * 1000);
    }
});

/**
 * Describes the theoretical length of a frame in milliseconds.
 *
 * @member {number} step
 * @memberof rune.system.Time
 * @instance
 * @readonly
 */
Object.defineProperty(rune.system.Time.prototype, "step", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return this.m_step;
    }
});

/**
 * Target framerate.
 *
 * @member {number} framerate
 * @memberof rune.system.Time
 * @instance
 */
Object.defineProperty(rune.system.Time.prototype, "targetFramerate", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return Math.ceil(1000 / this.m_step);
    },

    /**
     * @this rune.system.Time
     * @ignore
     */
    set: function(value) {
        this.m_step = 1000 / value;
    }
});

/**
 * Represents the update stack, ie a list of methods that aim to perform 
 * the necessary calculations for each frame. This stack is always executed 
 * before the rendering stack.
 *
 * @member {rune.util.Stack} update
 * @memberof rune.system.Time
 * @instance
 * @readonly
 */
Object.defineProperty(rune.system.Time.prototype, "update", {
    /**
     * @this rune.system.Time
     * @ignore
     */
    get: function() {
        return this.m_update;
    }
});

//------------------------------------------------------------------------------
// Public prototype methods (API)
//------------------------------------------------------------------------------

/**
 * Resets the update and rendering loop.
 *
 * @return {undefined}
 */
rune.system.Time.prototype.reset = function() {
    this.m_currentTime  = window.performance.now();
    this.m_previousTime = this.m_currentTime - this.m_step;
};

/**
 * Starts the update and rendering loop.
 *
 * @return {undefined}
 */
rune.system.Time.prototype.start = function() {
    this.m_initTimeLoop();
};

/**
 * Stops the update and rendering loop.
 *
 * @param {boolean} [clear=false] Whether the update and render stacks should be emptied or not.
 *
 * @return {undefined}
 */
rune.system.Time.prototype.stop = function(clear) {
    this.m_disposeTimeLoop();
    if (clear === true) {
        this.m_update.clear();
        this.m_render.clear();
    }
};

//------------------------------------------------------------------------------
// Public prototype methods (ENGINE)
//------------------------------------------------------------------------------

/**
 * Stops and deletes all memory allocations associated with the object.
 *
 * @returns {undefined}
 * @ignore
 */
rune.system.Time.prototype.dispose = function() {
    this.stop(true);
};

//------------------------------------------------------------------------------
// Private prototype methods
//------------------------------------------------------------------------------

/**
 * Starts the loop that represents time.
 *
 * @throws {Error} If the runtime environment does not support requestAnimationFrame.
 *
 * @return {undefined}
 * @private
 */
rune.system.Time.prototype.m_initTimeLoop = function() {
    if (window.requestAnimationFrame !== undefined) {
        this.m_tick = this.m_tick.bind(this);
        this.m_timeLoopID = window.requestAnimationFrame(this.m_tick);
    } else throw new Error();
};

/**
 * Calculates tick.
 *
 * @return {undefined}
 * @private
 */
rune.system.Time.prototype.m_tick = function() {
    this.m_previousTime = this.m_currentTime;
    this.m_currentTime  = window.performance.now();

    this.m_buffer += this.m_currentTime - this.m_previousTime;

    if (this.m_buffer > this.m_step) {
        
        while(this.m_ticks.length > 0 && this.m_ticks[0] <= this.m_currentTime - 1000) {
            this.m_ticks.shift();
        }
        
        this.m_ticks.push(this.m_currentTime);
        this.m_currentFramerate = this.m_ticks.length;
        
        while (this.m_buffer > this.m_step) {
            this.m_buffer -= this.m_step;
            this.m_execUpdateStack();
        }
        
        this.m_execRenderStack();
    }
    
    this.m_requestID = window.requestAnimationFrame(this.m_tick);
};

/**
 * Executes the contents of the update stack.
 *
 * @throws {Error} On missing stack.
 *
 * @return {undefined}
 * @private
 */
rune.system.Time.prototype.m_execUpdateStack = function() {
    if (this.m_update != null) {
        this.m_update.execute(this['step']);
    } else throw new Error();
};

/**
 * Executes the contents of the render stack.
 *
 * @throws {Error} On missing stack.
 *
 * @return {undefined}
 * @private
 */
rune.system.Time.prototype.m_execRenderStack = function() {
    if (this.m_render != null) {
        this.m_render.execute();
    } else throw new Error();
};

/**
 * Stops the loop that represents time.
 *
 * @throws {Error} If the runtime environment does not support requestAnimationFrame.
 *
 * @return {undefined}
 * @private
 */
rune.system.Time.prototype.m_disposeTimeLoop = function() {
    if (window.cancelAnimationFrame !== undefined) {
        window.cancelAnimationFrame(this.m_timeLoopID);
        this.m_timeLoopID = 0;
    } else throw new Error();
};