diff --git a/src/extras/MovieClip.js b/src/extras/MovieClip.js index d07ee87..948cff1 100644 --- a/src/extras/MovieClip.js +++ b/src/extras/MovieClip.js @@ -131,7 +131,7 @@ } this.playing = false; - Ticker.off('tick', this.update, this); + Ticker.sharedTicker.remove(this.update); }; /** @@ -146,7 +146,7 @@ } this.playing = true; - Ticker.on('tick', this.update, this); + Ticker.sharedTicker.add(this.update, this); }; /** diff --git a/src/extras/MovieClip.js b/src/extras/MovieClip.js index d07ee87..948cff1 100644 --- a/src/extras/MovieClip.js +++ b/src/extras/MovieClip.js @@ -131,7 +131,7 @@ } this.playing = false; - Ticker.off('tick', this.update, this); + Ticker.sharedTicker.remove(this.update); }; /** @@ -146,7 +146,7 @@ } this.playing = true; - Ticker.on('tick', this.update, this); + Ticker.sharedTicker.add(this.update, this); }; /** diff --git a/src/extras/Ticker.js b/src/extras/Ticker.js index e938c64..973d5a6 100644 --- a/src/extras/Ticker.js +++ b/src/extras/Ticker.js @@ -1,40 +1,83 @@ -var EventEmitter = require('eventemitter3').EventEmitter; +var EventEmitter = require('eventemitter3').EventEmitter, + performance = global.performance, + TICK = 'tick'; /** - * A Ticker class that runs an update loop that other objects listen to + * 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 */ -var Ticker = function() +function Ticker() { - EventEmitter.call(this); - - this.updateBind = this.update.bind(this); + 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 runs + * 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.active = false; + 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 between two frames - * - * @member {number} - */ - this.timeElapsed = 0; - - /** * The time at the last frame + * @todo better description * * @member {number} */ @@ -42,75 +85,201 @@ /** * The speed + * @todo better description * * @member {number} */ this.speed = 1; - // auto start ticking! - this.start(); -}; - -Ticker.prototype = Object.create(EventEmitter.prototype); -Ticker.prototype.constructor = Ticker; + /** + * The maximum time between 2 frames + * @todo better description + * + * @member {number} + */ + this.maxTimeElapsed = 100; +} /** - * Starts the ticker, automatically called by the constructor + * 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.start = function() +Ticker.prototype._startIfPossible = function _startIfPossible() { - if(this.active) + if (this.started) { - return; + 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(); } - this.active = true; - requestAnimationFrame(this.updateBind); + return this; }; /** - * Stops the ticker + * Starts the ticker. If the ticker has listeners + * a new animation frame is requested at this point. * + * @returns {PIXI.extras.Ticker} this */ -Ticker.prototype.stop = function() +Ticker.prototype.start = function start() { - if(!this.active) + if (!this.started) { - return; + this.started = true; + this._startIfPossible(); } - this.active = false; + return this; }; /** - * The update loop, fires the 'tick' event + * Stops the ticker. * + * @returns {PIXI.extras.Ticker} this */ -Ticker.prototype.update = function() +Ticker.prototype.stop = function stop() { - if(this.active) + if (this.started) { - requestAnimationFrame(this.updateBind); + this.started = false; + this._cancelIfNeeded(); + } - var currentTime = new Date().getTime(); - var timeElapsed = currentTime - this.lastTime; + 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! - if(timeElapsed > 100) + // TODO: Is this there a better way to do this? + if (timeElapsed > this.maxTimeElapsed) { - timeElapsed = 100; + 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; - this.emit('tick', this.deltaTime); + // 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); + } }; -module.exports = new Ticker(); +/** + * 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;