diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..9ac7173 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,6 +45,12 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** * @namespace PIXI.ticker diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..9ac7173 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,6 +45,12 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** * @namespace PIXI.ticker diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..9ac7173 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,6 +45,12 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** * @namespace PIXI.ticker diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d84580..b4fdf04 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -521,7 +521,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..9ac7173 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,6 +45,12 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** * @namespace PIXI.ticker diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d84580..b4fdf04 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -521,7 +521,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index 5afed00..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -158,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -228,7 +228,7 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..9ac7173 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,6 +45,12 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** * @namespace PIXI.ticker diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d84580..b4fdf04 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -521,7 +521,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index 5afed00..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -158,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -228,7 +228,7 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..776192e --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,302 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); +}); diff --git a/src/core/Application.js b/src/core/Application.js index 4effe11..7e952cb 100644 --- a/src/core/Application.js +++ b/src/core/Application.js @@ -2,6 +2,7 @@ import Container from './display/Container'; import { shared, Ticker } from './ticker'; import settings from './settings'; +import { UPDATE_PRIORITY } from './const'; /** * Convenience class to create a new PIXI application. @@ -97,7 +98,7 @@ this._ticker = ticker; if (ticker) { - ticker.add(this.render, this); + ticker.add(this.render, this, UPDATE_PRIORITY.LOW); } } get ticker() // eslint-disable-line require-jsdoc @@ -155,9 +156,12 @@ */ destroy(removeView) { - this.stop(); + const oldTicker = this._ticker; + this.ticker = null; + oldTicker.destroy(); + this.stage.destroy(); this.stage = null; diff --git a/src/core/const.js b/src/core/const.js index f9235e4..a26d950 100644 --- a/src/core/const.js +++ b/src/core/const.js @@ -309,3 +309,27 @@ LINEAR_VERTICAL: 0, LINEAR_HORIZONTAL: 1, }; + +/** + * Represents the update priorities used by internal PIXI classes when registered with + * the {@link PIXI.ticker.Ticker} object. Higher priority items are updated first and lower + * priority items, such as render, should go later. + * + * @static + * @constant + * @name UPDATE_PRIORITY + * @memberof PIXI + * @type {object} + * @property {number} INTERACTION=50 Highest priority, used for {@link PIXI.interaction.InteractionManager} + * @property {number} HIGH=25 High priority updating, {@link PIXI.VideoBaseTexture} and {@link PIXI.extras.AnimatedSprite} + * @property {number} NORMAL=0 Default priority for ticker events, see {@link PIXI.ticker.Ticker#add}. + * @property {number} LOW=-25 Low priority used for {@link PIXI.Application} rendering. + * @property {number} UTILITY=-50 Lowest priority used for {@link PIXI.prepare.BasePrepare} utility. + */ +export const UPDATE_PRIORITY = { + INTERACTION: 50, + HIGH: 25, + NORMAL: 0, + LOW: -25, + UTILITY: -50, +}; diff --git a/src/core/textures/VideoBaseTexture.js b/src/core/textures/VideoBaseTexture.js index 59eb9fd..a9b9f51 100644 --- a/src/core/textures/VideoBaseTexture.js +++ b/src/core/textures/VideoBaseTexture.js @@ -1,6 +1,7 @@ import BaseTexture from './BaseTexture'; import { uid, BaseTextureCache } from '../utils'; -import * as ticker from '../ticker'; +import { shared } from '../ticker'; +import { UPDATE_PRIORITY } from '../const'; /** * A texture of a [playing] Video. @@ -125,7 +126,7 @@ if (!this._isAutoUpdating && this.autoUpdate) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } @@ -139,7 +140,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } } @@ -187,7 +188,7 @@ { if (this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); } if (this.source && this.source._pixiId) @@ -281,12 +282,12 @@ if (!this._autoUpdate && this._isAutoUpdating) { - ticker.shared.remove(this.update, this); + shared.remove(this.update, this); this._isAutoUpdating = false; } else if (this._autoUpdate && !this._isAutoUpdating) { - ticker.shared.add(this.update, this); + shared.add(this.update, this, UPDATE_PRIORITY.HIGH); this._isAutoUpdating = true; } } diff --git a/src/core/ticker/Ticker.js b/src/core/ticker/Ticker.js index 17a1517..f2a498a 100644 --- a/src/core/ticker/Ticker.js +++ b/src/core/ticker/Ticker.js @@ -1,12 +1,10 @@ import settings from '../settings'; -import EventEmitter from 'eventemitter3'; - -// Internal event used by composed emitter -const TICK = 'tick'; +import { UPDATE_PRIORITY } from '../const'; +import TickerListener from './TickerListener'; /** * A Ticker class that runs an update loop that other objects listen to. - * This class is composed around an EventEmitter object to add listeners + * This class is composed around 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. @@ -22,10 +20,11 @@ constructor() { /** - * Internal emitter used to fire 'tick' event + * The first listener. All new listeners added are chained on this. * @private + * @type {TickerListener} */ - this._emitter = new EventEmitter(); + this._head = new TickerListener(null, null, Infinity); /** * Internal current frame request ID @@ -131,7 +130,7 @@ // Invoke listeners now this.update(time); // Listener side effects may have modified ticker state. - if (this.started && this._requestId === null && this._emitter.listeners(TICK, true)) + if (this.started && this._requestId === null && this._head.next) { this._requestId = requestAnimationFrame(this._tick); } @@ -148,7 +147,7 @@ */ _requestIfNeeded() { - if (this._requestId === null && this._emitter.listeners(TICK, true)) + if (this._requestId === null && this._head.next) { // ensure callbacks get correct delta this.lastTime = performance.now(); @@ -193,35 +192,72 @@ } /** - * 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. + * Register a handler for tick events. Calls continuously unless + * it is removed or the ticker is stopped. * * @param {Function} fn - The listener function to be added for updates * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - add(fn, context) + add(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.on(TICK, fn, context); - - this._startIfPossible(); - - return this; + return this._addListener(new TickerListener(fn, context, priority)); } /** - * 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. + * Add a handler for the tick event which is only execute once. * * @param {Function} fn - The listener function to be added for one update * @param {Function} [context] - The listener context + * @param {number} [priority=PIXI.UPDATE_PRIORITY.NORMAL] - The priority for emitting * @returns {PIXI.ticker.Ticker} This instance of a ticker */ - addOnce(fn, context) + addOnce(fn, context, priority = UPDATE_PRIORITY.NORMAL) { - this._emitter.once(TICK, fn, context); + return this._addListener(new TickerListener(fn, context, priority, true)); + } + + /** + * Internally adds the event handler so that it can be sorted by priority. + * Priority allows certain handler (user, AnimatedSprite, Interaction) to be run + * before the rendering. + * + * @private + * @param {TickerListener} listener - Current listener being added. + * @returns {PIXI.ticker.Ticker} This instance of a ticker + */ + _addListener(listener) + { + // For attaching to head + let current = this._head.next; + let previous = this._head; + + // Add the first item + if (!current) + { + listener.connect(previous); + } + else + { + // Go from highest to lowest priority + while (current) + { + if (listener.priority >= current.priority) + { + listener.connect(previous); + break; + } + previous = current; + current = current.next; + } + + // Not yet connected + if (!listener.previous) + { + listener.connect(previous); + } + } this._startIfPossible(); @@ -229,19 +265,33 @@ } /** - * 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. + * Removes any handlers matching the function and context parameters. + * If no handlers are left after removing, then it cancels the animation frame. * - * @param {Function} [fn] - The listener function to be removed + * @param {Function} fn - The listener function to be removed * @param {Function} [context] - The listener context to be removed * @returns {PIXI.ticker.Ticker} This instance of a ticker */ remove(fn, context) { - this._emitter.off(TICK, fn, context); + let listener = this._head.next; - if (!this._emitter.listeners(TICK, true)) + while (listener) + { + // We found a match, lets remove it + // no break to delete all possible matches + // incase a listener was added 2+ times + if (listener.match(fn, context)) + { + listener = listener.destroy(); + } + else + { + listener = listener.next; + } + } + + if (!this._head.next) { this._cancelIfNeeded(); } @@ -276,6 +326,25 @@ } /** + * Destroy the ticker and don't use after this. Calling + * this method removes all references to internal events. + */ + destroy() + { + this.stop(); + + let listener = this._head.next; + + while (listener) + { + listener = listener.destroy(true); + } + + this._head.destroy(); + this._head = null; + } + + /** * Triggers an update. An update entails setting the * current {@link PIXI.ticker.Ticker#elapsedMS}, * the current {@link PIXI.ticker.Ticker#deltaTime}, @@ -321,7 +390,17 @@ this.deltaTime = elapsedMS * settings.TARGET_FPMS * this.speed; // Invoke listeners added to internal emitter - this._emitter.emit(TICK, this.deltaTime); + let listener = this._head.next; + + while (listener) + { + listener = listener.emit(this.deltaTime); + } + + if (!this._head.next) + { + this._cancelIfNeeded(); + } } else { diff --git a/src/core/ticker/TickerListener.js b/src/core/ticker/TickerListener.js new file mode 100644 index 0000000..2bedb34 --- /dev/null +++ b/src/core/ticker/TickerListener.js @@ -0,0 +1,158 @@ +/** + * Internal class for handling the priority sorting of ticker handlers. + * + * @private + * @class + * @memberof PIXI.ticker + */ +export default class TickerListener +{ + /** + * Constructor + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} [context=null] - The listener context + * @param {number} [priority=0] - The priority for emitting + * @param {boolean} [once=false] - If the handler should fire once + */ + constructor(fn, context = null, priority = 0, once = false) + { + /** + * The handler function to execute. + * @member {Function} + */ + this.fn = fn; + + /** + * The calling to execute. + * @member {Function} + */ + this.context = context; + + /** + * The current priority. + * @member {number} + */ + this.priority = priority; + + /** + * If this should only execute once. + * @member {boolean} + */ + this.once = once; + + /** + * The next item in chain. + * @member {TickerListener} + */ + this.next = null; + + /** + * The previous item in chain. + * @member {TickerListener} + */ + this.previous = null; + + /** + * `true` if this listener has been destroyed already. + * @member {boolean} + * @private + */ + this._destroyed = false; + } + + /** + * Simple compare function to figure out if a function and context match. + * + * @param {Function} fn - The listener function to be added for one update + * @param {Function} context - The listener context + * @return {boolean} `true` if the listener match the arguments + */ + match(fn, context) + { + context = context || null; + + return this.fn === fn && this.context === context; + } + + /** + * Emit by calling the current function. + * @param {number} deltaTime - time since the last emit. + * @return {TickerListener} Next ticker + */ + emit(deltaTime) + { + if (this.context) + { + this.fn.call(this.context, deltaTime); + } + else + { + this.fn(deltaTime); + } + + if (this.once) + { + this.destroy(); + } + + const redirect = this.next; + + // Soft-destroying should remove + // the next reference + if (this._destroyed) + { + this.next = null; + } + + return redirect; + } + + /** + * Connect to the list. + * @param {TickerListener} previous - Input node, previous listener + */ + connect(previous) + { + this.previous = previous; + if (previous.next) + { + previous.next.previous = this; + } + this.next = previous.next; + previous.next = this; + } + + /** + * Destroy and don't use after this. + * @param {boolean} [hard = false] `true` to remove the `next` reference, this + * is considered a hard destroy. Soft destroy maintains the next reference. + * @return {TickerListener} The listener to redirect while emitting or removing. + */ + destroy(hard = false) + { + this._destroyed = true; + this.fn = null; + this.context = null; + + // Disconnect, hook up next and previous + if (this.previous) + { + this.previous.next = this.next; + } + + if (this.next) + { + this.next.previous = this.previous; + } + + // Redirect to the next item + const redirect = this.previous; + + // Remove references + this.next = hard ? null : redirect; + this.previous = null; + + return redirect; + } +} diff --git a/src/core/ticker/index.js b/src/core/ticker/index.js index 4b3017c..9ac7173 100644 --- a/src/core/ticker/index.js +++ b/src/core/ticker/index.js @@ -45,6 +45,12 @@ const shared = new Ticker(); shared.autoStart = true; +shared.destroy = () => +{ + // protect destroying shared ticker + // this is used by other internal systems + // like AnimatedSprite and InteractionManager +}; /** * @namespace PIXI.ticker diff --git a/src/extras/AnimatedSprite.js b/src/extras/AnimatedSprite.js index 13b804f..5297778 100644 --- a/src/extras/AnimatedSprite.js +++ b/src/extras/AnimatedSprite.js @@ -137,7 +137,7 @@ this.playing = true; if (this._autoUpdate) { - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.HIGH); } } diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d84580..b4fdf04 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -521,7 +521,7 @@ return; } - core.ticker.shared.add(this.update, this); + core.ticker.shared.add(this.update, this, core.UPDATE_PRIORITY.INTERACTION); if (window.navigator.msPointerEnabled) { diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js index 5afed00..e45c4dc 100644 --- a/src/prepare/BasePrepare.js +++ b/src/prepare/BasePrepare.js @@ -158,7 +158,7 @@ if (!this.ticking) { this.ticking = true; - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } else if (done) @@ -228,7 +228,7 @@ else { // if we are not finished, on the next rAF do this again - SharedTicker.addOnce(this.tick, this); + SharedTicker.addOnce(this.tick, this, core.UPDATE_PRIORITY.UTILITY); } } diff --git a/test/core/Ticker.js b/test/core/Ticker.js new file mode 100644 index 0000000..776192e --- /dev/null +++ b/test/core/Ticker.js @@ -0,0 +1,302 @@ +'use strict'; + +const Ticker = PIXI.ticker.Ticker; +const shared = PIXI.ticker.shared; + +describe('PIXI.ticker.Ticker', function () +{ + before(function () + { + this.length = (ticker) => + { + ticker = ticker || shared; + + if (!ticker._head || !ticker._head.next) + { + return 0; + } + + let listener = ticker._head.next; + let i = 0; + + while (listener) + { + listener = listener.next; + i++; + } + + return i; + }; + }); + + it('should be available', function () + { + expect(Ticker).to.be.a.function; + expect(shared).to.be.an.instanceof(Ticker); + }); + + it('should create a new ticker and destroy it', function () + { + const ticker = new Ticker(); + + ticker.start(); + + const listener = sinon.spy(); + + expect(this.length(ticker)).to.equal(0); + + ticker.add(listener); + + expect(this.length(ticker)).to.equal(1); + + ticker.destroy(); + + expect(ticker._head).to.be.null; + expect(ticker.started).to.be.false; + expect(this.length(ticker)).to.equal(0); + }); + + it('should protect destroying shared ticker', function () + { + shared.destroy(); + expect(shared._head).to.not.be.null; + expect(shared.started).to.be.true; + }); + + it('should add and remove listener', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener); + + expect(this.length()).to.equal(length + 1); + + shared.remove(listener); + + expect(this.length()).to.equal(length); + }); + + it('should update a listener', function () + { + const listener = sinon.spy(); + + shared.add(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + + shared.remove(listener); + shared.update(); + + expect(listener.calledOnce).to.be.true; + }); + + it('should update a listener twice and remove once', function () + { + const listener = sinon.spy(); + const length = this.length(); + + shared.add(listener).add(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length + 2); + + shared.remove(listener); + shared.update(); + + expect(listener.calledTwice).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should respect priority order', function () + { + const length = this.length(); + const listener1 = sinon.spy(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.LOW) + .add(listener4, null, PIXI.UPDATE_PRIORITY.INTERACTION) + .add(listener3, null, PIXI.UPDATE_PRIORITY.HIGH) + .add(listener2, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 4); + + sinon.assert.callOrder(listener4, listener3, listener2, listener1); + + shared.remove(listener1) + .remove(listener2) + .remove(listener3) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); + + it('should auto-remove once listeners', function () + { + const length = this.length(); + const listener = sinon.spy(); + + shared.addOnce(listener); + + shared.update(); + + expect(listener.calledOnce).to.be.true; + expect(this.length()).to.equal(length); + }); + + it('should call inserted item with a lower priority', function () + { + const length = this.length(); + const lowListener = sinon.spy(); + const highListener = sinon.spy(); + const mainListener = sinon.spy(() => + { + shared.add(highListener, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(lowListener, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(mainListener, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 3); + + expect(mainListener.calledOnce).to.be.true; + expect(lowListener.calledOnce).to.be.true; + expect(highListener.calledOnce).to.be.false; + + shared.remove(mainListener) + .remove(highListener) + .remove(lowListener); + + expect(this.length()).to.equal(length); + }); + + it('should remove emit low-priority item during emit', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener1) + .remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself on emit after adding new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + shared.remove(listener1); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove itself before, still calling new item', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener1 = sinon.spy(() => + { + shared.remove(listener1); + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.LOW); + + // listener is removed right away + expect(this.length()).to.equal(length + 1); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 1); + + expect(listener2.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.remove(listener2); + + expect(this.length()).to.equal(length); + }); + + it('should remove items before and after current priority', function () + { + const length = this.length(); + const listener2 = sinon.spy(); + const listener3 = sinon.spy(); + const listener4 = sinon.spy(); + + shared.add(listener2, null, PIXI.UPDATE_PRIORITY.HIGH); + shared.add(listener3, null, PIXI.UPDATE_PRIORITY.LOW); + shared.add(listener4, null, PIXI.UPDATE_PRIORITY.LOW); + + const listener1 = sinon.spy(() => + { + shared.remove(listener2) + .remove(listener3); + + // listener is removed right away + expect(this.length()).to.equal(length + 2); + }); + + shared.add(listener1, null, PIXI.UPDATE_PRIORITY.NORMAL); + + shared.update(); + + expect(this.length()).to.equal(length + 2); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledOnce).to.be.true; + expect(listener1.calledOnce).to.be.true; + + shared.update(); + + expect(listener2.calledOnce).to.be.true; + expect(listener3.calledOnce).to.be.false; + expect(listener4.calledTwice).to.be.true; + expect(listener1.calledTwice).to.be.true; + + shared.remove(listener1) + .remove(listener4); + + expect(this.length()).to.equal(length); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 7d159d7..58dc30e 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -28,4 +28,5 @@ require('./WebGLRenderer'); require('./Ellipse'); require('./Texture'); +require('./Ticker'); require('./filters');