diff --git a/src/core/textures/RenderTexture.js b/src/core/textures/RenderTexture.js index d2c27eb..93d8c75 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -428,3 +428,62 @@ return this.textureBuffer.canvas; } }; + +/** + * Will return a one-dimensional array containing the pixel data of the entire texture in RGBA order, with integer values between 0 and 255 (included). + * + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixels = function () +{ + var width, height; + + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + width = this.textureBuffer.size.width; + height = this.textureBuffer.size.height; + + var webGLPixels = new Uint8Array(4 * width * height); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + width = this.textureBuffer.canvas.width; + height = this.textureBuffer.canvas.height; + + return this.textureBuffer.canvas.getContext('2d').getImageData(0, 0, width, height).data; + } +}; + +/** + * Will return a one-dimensional array containing the pixel data of a pixel within the texture in RGBA order, with integer values between 0 and 255 (included). + * + * @param x {number} The x coordinate of the pixel to retrieve. + * @param y {number} The y coordinate of the pixel to retrieve. + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixel = function (x, y) +{ + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + + var webGLPixels = new Uint8Array(4); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + return this.textureBuffer.canvas.getContext('2d').getImageData(x, y, 1, 1).data; + } +}; diff --git a/src/core/textures/RenderTexture.js b/src/core/textures/RenderTexture.js index d2c27eb..93d8c75 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -428,3 +428,62 @@ return this.textureBuffer.canvas; } }; + +/** + * Will return a one-dimensional array containing the pixel data of the entire texture in RGBA order, with integer values between 0 and 255 (included). + * + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixels = function () +{ + var width, height; + + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + width = this.textureBuffer.size.width; + height = this.textureBuffer.size.height; + + var webGLPixels = new Uint8Array(4 * width * height); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + width = this.textureBuffer.canvas.width; + height = this.textureBuffer.canvas.height; + + return this.textureBuffer.canvas.getContext('2d').getImageData(0, 0, width, height).data; + } +}; + +/** + * Will return a one-dimensional array containing the pixel data of a pixel within the texture in RGBA order, with integer values between 0 and 255 (included). + * + * @param x {number} The x coordinate of the pixel to retrieve. + * @param y {number} The y coordinate of the pixel to retrieve. + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixel = function (x, y) +{ + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + + var webGLPixels = new Uint8Array(4); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + return this.textureBuffer.canvas.getContext('2d').getImageData(x, y, 1, 1).data; + } +}; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ff665a0..3d6c9c2 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -239,8 +239,8 @@ this.baseTexture.destroy(); } - this.baseTexture.remove('update', this.onBaseTextureUpdated, this); - this.baseTexture.remove('loaded', this.onBaseTextureLoaded, this); + this.baseTexture.removeListener('update', this.onBaseTextureUpdated, this); + this.baseTexture.removeListener('loaded', this.onBaseTextureLoaded, this); this.valid = false; }; diff --git a/src/core/textures/RenderTexture.js b/src/core/textures/RenderTexture.js index d2c27eb..93d8c75 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -428,3 +428,62 @@ return this.textureBuffer.canvas; } }; + +/** + * Will return a one-dimensional array containing the pixel data of the entire texture in RGBA order, with integer values between 0 and 255 (included). + * + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixels = function () +{ + var width, height; + + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + width = this.textureBuffer.size.width; + height = this.textureBuffer.size.height; + + var webGLPixels = new Uint8Array(4 * width * height); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + width = this.textureBuffer.canvas.width; + height = this.textureBuffer.canvas.height; + + return this.textureBuffer.canvas.getContext('2d').getImageData(0, 0, width, height).data; + } +}; + +/** + * Will return a one-dimensional array containing the pixel data of a pixel within the texture in RGBA order, with integer values between 0 and 255 (included). + * + * @param x {number} The x coordinate of the pixel to retrieve. + * @param y {number} The y coordinate of the pixel to retrieve. + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixel = function (x, y) +{ + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + + var webGLPixels = new Uint8Array(4); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + return this.textureBuffer.canvas.getContext('2d').getImageData(x, y, 1, 1).data; + } +}; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ff665a0..3d6c9c2 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -239,8 +239,8 @@ this.baseTexture.destroy(); } - this.baseTexture.remove('update', this.onBaseTextureUpdated, this); - this.baseTexture.remove('loaded', this.onBaseTextureLoaded, this); + this.baseTexture.removeListener('update', this.onBaseTextureUpdated, this); + this.baseTexture.removeListener('loaded', this.onBaseTextureLoaded, this); this.valid = false; }; diff --git a/src/extras/MovieClip.js b/src/extras/MovieClip.js index d07ee87..6a34990 100644 --- a/src/extras/MovieClip.js +++ b/src/extras/MovieClip.js @@ -27,9 +27,7 @@ core.Sprite.call(this, textures[0]); /** - * The array of textures that make up the animation - * - * @member {Texture[]} + * @private */ this._textures = textures; @@ -100,7 +98,7 @@ /** * The array of textures used for this MovieClip * - * @member + * @member {Texture[]} * @memberof MovieClip# * */ @@ -131,7 +129,7 @@ } this.playing = false; - Ticker.off('tick', this.update, this); + Ticker.sharedTicker.remove(this.update); }; /** @@ -146,7 +144,7 @@ } this.playing = true; - Ticker.on('tick', this.update, this); + Ticker.sharedTicker.add(this.update, this); }; /** diff --git a/src/core/textures/RenderTexture.js b/src/core/textures/RenderTexture.js index d2c27eb..93d8c75 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -428,3 +428,62 @@ return this.textureBuffer.canvas; } }; + +/** + * Will return a one-dimensional array containing the pixel data of the entire texture in RGBA order, with integer values between 0 and 255 (included). + * + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixels = function () +{ + var width, height; + + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + width = this.textureBuffer.size.width; + height = this.textureBuffer.size.height; + + var webGLPixels = new Uint8Array(4 * width * height); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + width = this.textureBuffer.canvas.width; + height = this.textureBuffer.canvas.height; + + return this.textureBuffer.canvas.getContext('2d').getImageData(0, 0, width, height).data; + } +}; + +/** + * Will return a one-dimensional array containing the pixel data of a pixel within the texture in RGBA order, with integer values between 0 and 255 (included). + * + * @param x {number} The x coordinate of the pixel to retrieve. + * @param y {number} The y coordinate of the pixel to retrieve. + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixel = function (x, y) +{ + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + + var webGLPixels = new Uint8Array(4); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + return this.textureBuffer.canvas.getContext('2d').getImageData(x, y, 1, 1).data; + } +}; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ff665a0..3d6c9c2 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -239,8 +239,8 @@ this.baseTexture.destroy(); } - this.baseTexture.remove('update', this.onBaseTextureUpdated, this); - this.baseTexture.remove('loaded', this.onBaseTextureLoaded, this); + this.baseTexture.removeListener('update', this.onBaseTextureUpdated, this); + this.baseTexture.removeListener('loaded', this.onBaseTextureLoaded, this); this.valid = false; }; diff --git a/src/extras/MovieClip.js b/src/extras/MovieClip.js index d07ee87..6a34990 100644 --- a/src/extras/MovieClip.js +++ b/src/extras/MovieClip.js @@ -27,9 +27,7 @@ core.Sprite.call(this, textures[0]); /** - * The array of textures that make up the animation - * - * @member {Texture[]} + * @private */ this._textures = textures; @@ -100,7 +98,7 @@ /** * The array of textures used for this MovieClip * - * @member + * @member {Texture[]} * @memberof MovieClip# * */ @@ -131,7 +129,7 @@ } this.playing = false; - Ticker.off('tick', this.update, this); + Ticker.sharedTicker.remove(this.update); }; /** @@ -146,7 +144,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; diff --git a/src/core/textures/RenderTexture.js b/src/core/textures/RenderTexture.js index d2c27eb..93d8c75 100644 --- a/src/core/textures/RenderTexture.js +++ b/src/core/textures/RenderTexture.js @@ -428,3 +428,62 @@ return this.textureBuffer.canvas; } }; + +/** + * Will return a one-dimensional array containing the pixel data of the entire texture in RGBA order, with integer values between 0 and 255 (included). + * + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixels = function () +{ + var width, height; + + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + width = this.textureBuffer.size.width; + height = this.textureBuffer.size.height; + + var webGLPixels = new Uint8Array(4 * width * height); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + width = this.textureBuffer.canvas.width; + height = this.textureBuffer.canvas.height; + + return this.textureBuffer.canvas.getContext('2d').getImageData(0, 0, width, height).data; + } +}; + +/** + * Will return a one-dimensional array containing the pixel data of a pixel within the texture in RGBA order, with integer values between 0 and 255 (included). + * + * @param x {number} The x coordinate of the pixel to retrieve. + * @param y {number} The y coordinate of the pixel to retrieve. + * @return {Uint8ClampedArray} + */ +RenderTexture.prototype.getPixel = function (x, y) +{ + if (this.renderer.type === CONST.RENDERER_TYPE.WEBGL) + { + var gl = this.renderer.gl; + + var webGLPixels = new Uint8Array(4); + + gl.bindFramebuffer(gl.FRAMEBUFFER, this.textureBuffer.frameBuffer); + gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, webGLPixels); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + return webGLPixels; + } + else + { + return this.textureBuffer.canvas.getContext('2d').getImageData(x, y, 1, 1).data; + } +}; diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ff665a0..3d6c9c2 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -239,8 +239,8 @@ this.baseTexture.destroy(); } - this.baseTexture.remove('update', this.onBaseTextureUpdated, this); - this.baseTexture.remove('loaded', this.onBaseTextureLoaded, this); + this.baseTexture.removeListener('update', this.onBaseTextureUpdated, this); + this.baseTexture.removeListener('loaded', this.onBaseTextureLoaded, this); this.valid = false; }; diff --git a/src/extras/MovieClip.js b/src/extras/MovieClip.js index d07ee87..6a34990 100644 --- a/src/extras/MovieClip.js +++ b/src/extras/MovieClip.js @@ -27,9 +27,7 @@ core.Sprite.call(this, textures[0]); /** - * The array of textures that make up the animation - * - * @member {Texture[]} + * @private */ this._textures = textures; @@ -100,7 +98,7 @@ /** * The array of textures used for this MovieClip * - * @member + * @member {Texture[]} * @memberof MovieClip# * */ @@ -131,7 +129,7 @@ } this.playing = false; - Ticker.off('tick', this.update, this); + Ticker.sharedTicker.remove(this.update); }; /** @@ -146,7 +144,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; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index 9e2206d..52672d4 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,8 +1,63 @@ var Resource = require('resource-loader').Resource, core = require('../core'), + utils = require('../core/utils'), extras = require('../extras'), path = require('path'); + +function parse(resource, texture) { + var data = {}; + var info = resource.data.getElementsByTagName('info')[0]; + var common = resource.data.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + //parse letters + var letters = resource.data.getElementsByTagName('char'); + + for (var i = 0; i < letters.length; i++) + { + var charCode = parseInt(letters[i].getAttribute('id'), 10); + + var textureRect = new core.math.Rectangle( + parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, + parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, + parseInt(letters[i].getAttribute('width'), 10), + parseInt(letters[i].getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), + yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), + xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect) + + }; + } + + //parse kernings + var kernings = resource.data.getElementsByTagName('kerning'); + for (i = 0; i < kernings.length; i++) + { + var first = parseInt(kernings[i].getAttribute('first'), 10); + var second = parseInt(kernings[i].getAttribute('second'), 10); + var amount = parseInt(kernings[i].getAttribute('amount'), 10); + + data.chars[second].kerning[first] = amount; + } + + resource.bitmapFont = data; + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + extras.BitmapText.fonts[data.font] = data; +} + + module.exports = function () { return function (resource, next) @@ -51,68 +106,22 @@ if (xmlUrl && xmlUrl.charAt(xmlUrl.length - 1) !== '/') { xmlUrl += '/'; } - var textureUrl = xmlUrl + resource.data.getElementsByTagName('page')[0].getAttribute('file'); - var loadOptions = { - crossOrigin: resource.crossOrigin, - loadType: Resource.LOAD_TYPE.IMAGE - }; - - // load the texture for the font - this.add(resource.name + '_image', textureUrl, loadOptions, function (res) - { - var data = {}; - var info = resource.data.getElementsByTagName('info')[0]; - var common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - //parse letters - var letters = resource.data.getElementsByTagName('char'); - - for (var i = 0; i < letters.length; i++) - { - var charCode = parseInt(letters[i].getAttribute('id'), 10); - - var textureRect = new core.math.Rectangle( - parseInt(letters[i].getAttribute('x'), 10), - parseInt(letters[i].getAttribute('y'), 10), - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: core.utils.TextureCache[charCode] = new core.Texture(res.texture.baseTexture, textureRect) - - }; - } - - //parse kernings - var kernings = resource.data.getElementsByTagName('kerning'); - for (i = 0; i < kernings.length; i++) - { - var first = parseInt(kernings[i].getAttribute('first'), 10); - var second = parseInt(kernings[i].getAttribute('second'), 10); - var amount = parseInt(kernings[i].getAttribute('amount'), 10); - - data.chars[second].kerning[first] = amount; - - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - extras.BitmapText.fonts[data.font] = data; - + if (utils.TextureCache[textureUrl]) { + //reuse existing texture + parse(resource, utils.TextureCache[textureUrl]); next(); - }); + } + else { + var loadOptions = { + crossOrigin: resource.crossOrigin, + loadType: Resource.LOAD_TYPE.IMAGE + }; + // load the texture for the font + this.add(resource.name + '_image', textureUrl, loadOptions, function (res) { + parse(resource, res.texture); + next(); + }); + } }; };