var core = require('../core'), EventEmitter = require('eventemitter3').EventEmitter, // Internal event used by composed emitter 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. * Also separating frame requests from update method * so listeners may be called at any time and with * any animation API, just invoke ticker.update(time). * * @private */ this._tick = function _tick(time) { _this._requestId = null; if (_this.started) { _this.update(time); } // Check 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._requestId === null && hasListeners(_this._emitter)) { _this._requestId = requestAnimationFrame(_this._tick); } }; /** * Internal emitter used to fire 'tick' event * @private */ this._emitter = new EventEmitter(); /** * Internal current frame request ID * @private */ this._requestId = null; /** * Internal value managed by minFPS property setter and getter. * This is the maximum allowed millseconds between updates. * @private */ this._maxElapsedMS = 100; /** * Whether or not this ticker should * start automatically when a listener is added. * * @member {boolean} */ this.autoStart = false; /** * The current percentage of the * target FPS with speed factored in. * * @member {number} */ this.deltaTime = 1; /** * The last time {@link PIXI.extras.Ticker#update} * was invoked by animation frame callback or manually. * * @member {number} */ this.lastTime = 0; /** * Factor of current FPS. * @example * ticker.speed = 2; // Approximately 120 FPS, or 0.12 FPMS. * * @member {number} */ this.speed = 1; /** * 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; } Object.defineProperties(Ticker.prototype, { /** * Gets the frames per second for which this * ticker is running. The default is appoximately * 60 FPS in modern browsers, but may vary. * This also factors in the property value of * {@link PIXI.extras.Ticker#speed}. * * @member * @memberof PIXI.extras.Ticker# * @readonly */ FPS: { get: function() { return core.TARGET_FPMS * 1000 * this.deltaTime; } }, /** * This property manages the maximum amount * of time allowed to elapse between ticks, * or calls to {@link PIXI.extras.Ticker#update}. * * @member * @memberof PIXI.extras.Ticker# * @default 10 */ minFPS: { get: function() { return 1000 / this._maxElapsedMS; }, set: function(fps) { var minFPMS = Math.min(fps / 1000, core.TARGET_FPMS); this._maxElapsedMS = 1 / minFPMS; } } }); /** * Conditionally requests a new animation frame. * If a frame has not already been requested, and if the internal * emitter has listeners, a new frame is requested. * * @private */ Ticker.prototype._requestIfNeeded = function _requestIfNeeded() { if (this._requestId === null && hasListeners(this._emitter)) { // ensure callbacks get correct delta this.lastTime = performance.now(); this._requestId = requestAnimationFrame(this._tick); } }; /** * Conditionally cancels a pending animation frame. * * @private */ Ticker.prototype._cancelIfNeeded = function _cancelIfNeeded() { if (this._requestId !== null) { cancelAnimationFrame(this._requestId); this._requestId = null; } }; /** * 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) { this._requestIfNeeded(); } else if (this.autoStart) { this.start(); } }; /** * 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) { this._emitter.off(TICK, fn, once); 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. */ Ticker.prototype.start = function start() { if (!this.started) { this.started = true; this._requestIfNeeded(); } }; /** * Stops the ticker. If the ticker has requested * an animation frame it is canceled at this point. */ Ticker.prototype.stop = function stop() { if (this.started) { this.started = false; this._cancelIfNeeded(); } }; /** * Triggers an update, setting deltaTime, lastTime, and * firing the internal 'tick' event invoking all listeners. * * @param [currentTime=performance.now()] {number} the current time of execution */ Ticker.prototype.update = function update(currentTime) { var elapsedMS; // Allow calling update directly with default currentTime. currentTime = currentTime || performance.now(); elapsedMS = currentTime - this.lastTime; // cap the milliseconds elapsed if (elapsedMS > this._maxElapsedMS) { elapsedMS = this._maxElapsedMS; } this.deltaTime = (elapsedMS * core.TARGET_FPMS); // Factor in speed this.deltaTime *= this.speed; // Invoke listeners added to internal emitter this._emitter.emit(TICK, this.deltaTime); this.lastTime = currentTime; }; /** * 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;