Newer
Older
pixi.js / src / extras / Ticker.js
@Dr. Kibtiz Dr. Kibtiz on 18 Apr 2015 6 KB Refactor PIXI.extras.Ticker
var EventEmitter = require('eventemitter3').EventEmitter,
    performance = global.performance,
    TICK = 'tick';

/**
 * Yes, this is accessing an internal module:eventemitter3 api.
 * Ugly, but calling module:eventemitter3.EventEmitter#listeners
 * does a bit too much for what this is for.
 * This is simple enough to keep track of and contribute
 * back to the eventemitter3 project in the near future.
 *
 * @private
 */
function hasListeners(emitter)
{
    return !!(emitter._events && emitter._events[TICK]);
}

/**
 * A Ticker class that runs an update loop that other objects listen to.
 * This class is composed around an EventEmitter object to add listeners
 * meant for execution on the next requested animation frame.
 * Animation frames are requested only when necessary,
 * e.g. When the ticker is started and the emitter has listeners.
 *
 * @class
 * @memberof PIXI.extras
 */
function Ticker()
{
    var _this = this;
    /**
     * Internal tick method bound to ticker instance.
     * This is because in early 2015, Function.bind
     * is still 60% slower in high performance scenarios.
     *
     * @private
     */
    this._tick = function _tick(time) {
        _this.update(time);
    };
    /**
     * Internal emitter
     * @private
     */
    this._emitter = new EventEmitter();
    /**
     * Internal frame request reference
     * @private
     */
    this._rafId = null;

    /**
     * Whether or not this ticker has been started.
     * `true` if {@link PIXI.extras.Ticker.start} has been called.
     * `false` if {@link PIXI.extras.Ticker.stop} has been called.
     *
     * @member {boolean}
     */
    this.started = false;

    /**
     * Whether or not this ticker should
     * start automatically when a listener is added.
     *
     * @member {boolean}
     */
    this.autoStart = false;

    /**
     * The deltaTime
     * @todo better description
     *
     * @member {number}
     */
    this.deltaTime = 1;

    /**
     * The time at the last frame
     * @todo better description
     *
     * @member {number}
     */
    this.lastTime = 0;

    /**
     * The speed
     * @todo better description
     *
     * @member {number}
     */
    this.speed = 1;

    /**
     * The maximum time between 2 frames
     * @todo better description
     *
     * @member {number}
     */
    this.maxTimeElapsed = 100;
}

/**
 * Conditionally requests a new animation frame.
 * If the ticker has been started it checks if a frame has not already
 * been requested, and if the internal emitter has listeners. If these
 * conditions are met, a new frame is requested. If the ticker has not
 * been started, but autoStart is `true`, then the ticker starts now,
 * and continues with the previous conditions to request a new frame.
 *
 * @private
 */
Ticker.prototype._startIfPossible = function _startIfPossible()
{
    if (this.started)
    {
        if (this._rafId === null && hasListeners(this._emitter))
        {
            // ensure callbacks get correct delta
            this.lastTime = performance.now();
            this._rafId = requestAnimationFrame(this._tick);
        }
    }
    else if (this.autoStart)
    {
        this.start();
    }
};

/**
 * Conditionally cancels a pending animation frame.
 *
 * @private
 */
Ticker.prototype._cancelIfNeeded = function _cancelIfNeeded()
{
    if (this._rafId !== null)
    {
        cancelAnimationFrame(this._rafId);
        this._rafId = null;
    }
};

/**
 * Calls {@link module:eventemitter3.EventEmitter#on} internally for the
 * internal 'tick' event. It checks if the emitter has listeners,
 * and if so it requests a new animation frame at this point.
 *
 * @returns {PIXI.extras.Ticker} this
 */
Ticker.prototype.add = function add(fn, context)
{
    this._emitter.on(TICK, fn, context);

    this._startIfPossible();

    return this;
};

/**
 * Calls {@link module:eventemitter3.EventEmitter#once} internally for the
 * internal 'tick' event. It checks if the emitter has listeners,
 * and if so it requests a new animation frame at this point.
 *
 * @returns {PIXI.extras.Ticker} this
 */
Ticker.prototype.addOnce = function addOnce(fn, context)
{
    this._emitter.once(TICK, fn, context);

    this._startIfPossible();

    return this;
};

/**
 * Calls {@link module:eventemitter3.EventEmitter#off} internally for 'tick' event.
 * It checks if the emitter has listeners for 'tick' event.
 * If it does, then it cancels the animation frame.
 *
 * @returns {PIXI.extras.Ticker} this
 */
Ticker.prototype.remove = function remove(fn, once)
{
    // remove listener(s) from internal emitter
    this._emitter.off(TICK, fn, once);

    // If there are no listeners, cancel the request.
    if (!hasListeners(this._emitter))
    {
        this._cancelIfNeeded();
    }

    return this;
};

/**
 * Starts the ticker. If the ticker has listeners
 * a new animation frame is requested at this point.
 *
 * @returns {PIXI.extras.Ticker} this
 */
Ticker.prototype.start = function start()
{
    if (!this.started)
    {
        this.started = true;
        this._startIfPossible();
    }

    return this;
};

/**
 * Stops the ticker.
 *
 * @returns {PIXI.extras.Ticker} this
 */
Ticker.prototype.stop = function stop()
{
    if (this.started)
    {
        this.started = false;
        this._cancelIfNeeded();
    }

    return this;
};

/**
 * Triggers an update, setting deltaTime, lastTime, and
 * firing the internal 'tick' event. After this, if the
 * ticker is still started and has listeners,
 * another frame is requested.
 */
Ticker.prototype.update = function update(currentTime)
{
    var timeElapsed;

    this._rafId = null;

    if (this.started)
    {
        // Allow calling tick directly getting correct currentTime
        currentTime = currentTime || performance.now();
        timeElapsed = currentTime - this.lastTime;

        // cap the time!
        // TODO: Is this there a better way to do this?
        if (timeElapsed > this.maxTimeElapsed)
        {
            timeElapsed = this.maxTimeElapsed;
        }

        // TODO: Would love to know what to name this magic number 0.6
        this.deltaTime = (timeElapsed * 0.06);
        this.deltaTime *= this.speed;

        // Invoke listeners added to internal emitter
        this._emitter.emit(TICK, this.deltaTime);

        this.lastTime = currentTime;
    }

    // Check again here because listeners could have side effects
    // and may have modified state during frame execution.
    // A new frame may have been requested or listeners removed.
    if (this.started && this._rafId === null && hasListeners(this._emitter))
    {
        this._rafId = requestAnimationFrame(this._tick);
    }
};

/**
 * The shared ticker instance used by {@link PIXI.extras.MovieClip}.
 * The property {@link PIXI.extras.Ticker#autoStart} is set to true
 * for this instance.
 *
 * @type {PIXI.extras.Ticker}
 * @memberof PIXI.extras.Ticker
 */
Ticker.sharedTicker = new Ticker();
Ticker.sharedTicker.autoStart = true;

module.exports = Ticker;