diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 8d2b5a6..d6e5a70 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -1,7 +1,5 @@ import * as core from '../../core'; - -const SharedTicker = core.ticker.shared; -const DEFAULT_UPLOADS_PER_FRAME = 4; +import BasePrepare from '../BasePrepare'; /** * The prepare manager provides functionality to upload content to the GPU. @@ -9,241 +7,25 @@ * @class * @memberof PIXI */ -export default class WebGLPrepare +export default class WebGLPrepare extends BasePrepare { /** * @param {PIXI.WebGLRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.WebGLRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; + this.uploadHookHelper = this.renderer; // Add textures and graphics to upload this.register(findBaseTextures, uploadBaseTextures) .register(findGraphics, uploadGraphics); } - /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update. - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this.renderer, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `renderer:WebGLRenderer, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** - * Destroys the plugin, don't use after this. - * - */ - destroy() - { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; - } - } /** - * The number of graphics or textures to upload to the GPU - * - * @static - * @type {number} - * @default 4 - */ -WebGLPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private @@ -255,7 +37,13 @@ { if (item instanceof core.BaseTexture) { - renderer.textureManager.updateTexture(item); + // if the texture already has a GL texture, then the texture has been prepared or rendered + // before now. If the texture changed, then the changer should be calling texture.update() which + // reuploads the texture without need for preparing it again + if (!item._glTextures[renderer.CONTEXT_UID]) + { + renderer.textureManager.updateTexture(item); + } return true; } @@ -275,7 +63,12 @@ { if (item instanceof core.Graphics) { - renderer.plugins.graphics.updateGraphics(item); + // if the item is not dirty and already has webgl data, then it got prepared or rendered + // before now and we shouldn't waste time updating it again + if (item.dirty || item.clearDirty || !item._webGL[renderer.plugins.graphics.CONTEXT_UID]) + { + renderer.plugins.graphics.updateGraphics(item); + } return true; } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 8d2b5a6..d6e5a70 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -1,7 +1,5 @@ import * as core from '../../core'; - -const SharedTicker = core.ticker.shared; -const DEFAULT_UPLOADS_PER_FRAME = 4; +import BasePrepare from '../BasePrepare'; /** * The prepare manager provides functionality to upload content to the GPU. @@ -9,241 +7,25 @@ * @class * @memberof PIXI */ -export default class WebGLPrepare +export default class WebGLPrepare extends BasePrepare { /** * @param {PIXI.WebGLRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.WebGLRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; + this.uploadHookHelper = this.renderer; // Add textures and graphics to upload this.register(findBaseTextures, uploadBaseTextures) .register(findGraphics, uploadGraphics); } - /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update. - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this.renderer, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `renderer:WebGLRenderer, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** - * Destroys the plugin, don't use after this. - * - */ - destroy() - { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; - } - } /** - * The number of graphics or textures to upload to the GPU - * - * @static - * @type {number} - * @default 4 - */ -WebGLPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private @@ -255,7 +37,13 @@ { if (item instanceof core.BaseTexture) { - renderer.textureManager.updateTexture(item); + // if the texture already has a GL texture, then the texture has been prepared or rendered + // before now. If the texture changed, then the changer should be calling texture.update() which + // reuploads the texture without need for preparing it again + if (!item._glTextures[renderer.CONTEXT_UID]) + { + renderer.textureManager.updateTexture(item); + } return true; } @@ -275,7 +63,12 @@ { if (item instanceof core.Graphics) { - renderer.plugins.graphics.updateGraphics(item); + // if the item is not dirty and already has webgl data, then it got prepared or rendered + // before now and we shouldn't waste time updating it again + if (item.dirty || item.clearDirty || !item._webGL[renderer.plugins.graphics.CONTEXT_UID]) + { + renderer.plugins.graphics.updateGraphics(item); + } return true; } diff --git a/test/index.js b/test/index.js index 7424430..bf50692 100755 --- a/test/index.js +++ b/test/index.js @@ -14,4 +14,5 @@ require('./core'); require('./interaction'); require('./renders'); + require('./prepare'); }); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 8d2b5a6..d6e5a70 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -1,7 +1,5 @@ import * as core from '../../core'; - -const SharedTicker = core.ticker.shared; -const DEFAULT_UPLOADS_PER_FRAME = 4; +import BasePrepare from '../BasePrepare'; /** * The prepare manager provides functionality to upload content to the GPU. @@ -9,241 +7,25 @@ * @class * @memberof PIXI */ -export default class WebGLPrepare +export default class WebGLPrepare extends BasePrepare { /** * @param {PIXI.WebGLRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.WebGLRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; + this.uploadHookHelper = this.renderer; // Add textures and graphics to upload this.register(findBaseTextures, uploadBaseTextures) .register(findGraphics, uploadGraphics); } - /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update. - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this.renderer, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `renderer:WebGLRenderer, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** - * Destroys the plugin, don't use after this. - * - */ - destroy() - { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; - } - } /** - * The number of graphics or textures to upload to the GPU - * - * @static - * @type {number} - * @default 4 - */ -WebGLPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private @@ -255,7 +37,13 @@ { if (item instanceof core.BaseTexture) { - renderer.textureManager.updateTexture(item); + // if the texture already has a GL texture, then the texture has been prepared or rendered + // before now. If the texture changed, then the changer should be calling texture.update() which + // reuploads the texture without need for preparing it again + if (!item._glTextures[renderer.CONTEXT_UID]) + { + renderer.textureManager.updateTexture(item); + } return true; } @@ -275,7 +63,12 @@ { if (item instanceof core.Graphics) { - renderer.plugins.graphics.updateGraphics(item); + // if the item is not dirty and already has webgl data, then it got prepared or rendered + // before now and we shouldn't waste time updating it again + if (item.dirty || item.clearDirty || !item._webGL[renderer.plugins.graphics.CONTEXT_UID]) + { + renderer.plugins.graphics.updateGraphics(item); + } return true; } diff --git a/test/index.js b/test/index.js index 7424430..bf50692 100755 --- a/test/index.js +++ b/test/index.js @@ -14,4 +14,5 @@ require('./core'); require('./interaction'); require('./renders'); + require('./prepare'); }); diff --git a/test/prepare/BasePrepare.js b/test/prepare/BasePrepare.js new file mode 100644 index 0000000..03cf2fa --- /dev/null +++ b/test/prepare/BasePrepare.js @@ -0,0 +1,158 @@ +'use strict'; + +describe('PIXI.prepare.BasePrepare', function () +{ + it('should create a new, empty, BasePrepare', function () + { + const renderer = {}; + const prep = new PIXI.prepare.BasePrepare(renderer); + + expect(prep.renderer).to.equal(renderer); + expect(prep.uploadHookHelper).to.be.null; + expect(prep.queue).to.be.empty; + expect(prep.addHooks).to.have.lengthOf(2); + expect(prep.uploadHooks).to.have.lengthOf(2); + expect(prep.completes).to.be.empty; + + prep.destroy(); + }); + + it('should add hooks', function () + { + function addHook() { /* empty */ } + function uploadHook() { /* empty */ } + const prep = new PIXI.prepare.BasePrepare(); + + prep.register(addHook, uploadHook); + + expect(prep.addHooks).to.contain(addHook); + expect(prep.addHooks).to.have.lengthOf(3); + expect(prep.uploadHooks).to.contain(uploadHook); + expect(prep.uploadHooks).to.have.lengthOf(3); + + prep.destroy(); + }); + + it('should call hooks and complete', function () + { + const prep = new PIXI.prepare.BasePrepare(); + const uploadItem = {}; + const uploadHelper = {}; + + prep.uploadHookHelper = uploadHelper; + + const addHook = sinon.spy(function (item, queue) + { + expect(item).to.equal(uploadItem); + expect(queue).to.equal(prep.queue); + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function (helper, item) + { + expect(helper).to.equal(uploadHelper); + expect(item).to.equal(uploadItem); + + return true; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload(uploadItem, complete); + + expect(prep.queue).to.contain(uploadItem); + + prep.prepareItems(); + + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should call complete if no queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + function addHook() + { + return false; + } + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook); + prep.upload({}, complete); + + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should remove un-preparable items from queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return false; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + + prep.prepareItems(); + + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should attach to SharedTicker', function (done) + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return true; + }); + + function complete() + { + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + + prep.destroy(); + + done(); + } + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + expect(addHook.called).to.be.true; + expect(uploadHook.called).to.be.false; + expect(complete.called).to.not.be.ok; + }); +}); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 8d2b5a6..d6e5a70 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -1,7 +1,5 @@ import * as core from '../../core'; - -const SharedTicker = core.ticker.shared; -const DEFAULT_UPLOADS_PER_FRAME = 4; +import BasePrepare from '../BasePrepare'; /** * The prepare manager provides functionality to upload content to the GPU. @@ -9,241 +7,25 @@ * @class * @memberof PIXI */ -export default class WebGLPrepare +export default class WebGLPrepare extends BasePrepare { /** * @param {PIXI.WebGLRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.WebGLRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; + this.uploadHookHelper = this.renderer; // Add textures and graphics to upload this.register(findBaseTextures, uploadBaseTextures) .register(findGraphics, uploadGraphics); } - /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update. - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this.renderer, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `renderer:WebGLRenderer, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** - * Destroys the plugin, don't use after this. - * - */ - destroy() - { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; - } - } /** - * The number of graphics or textures to upload to the GPU - * - * @static - * @type {number} - * @default 4 - */ -WebGLPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private @@ -255,7 +37,13 @@ { if (item instanceof core.BaseTexture) { - renderer.textureManager.updateTexture(item); + // if the texture already has a GL texture, then the texture has been prepared or rendered + // before now. If the texture changed, then the changer should be calling texture.update() which + // reuploads the texture without need for preparing it again + if (!item._glTextures[renderer.CONTEXT_UID]) + { + renderer.textureManager.updateTexture(item); + } return true; } @@ -275,7 +63,12 @@ { if (item instanceof core.Graphics) { - renderer.plugins.graphics.updateGraphics(item); + // if the item is not dirty and already has webgl data, then it got prepared or rendered + // before now and we shouldn't waste time updating it again + if (item.dirty || item.clearDirty || !item._webGL[renderer.plugins.graphics.CONTEXT_UID]) + { + renderer.plugins.graphics.updateGraphics(item); + } return true; } diff --git a/test/index.js b/test/index.js index 7424430..bf50692 100755 --- a/test/index.js +++ b/test/index.js @@ -14,4 +14,5 @@ require('./core'); require('./interaction'); require('./renders'); + require('./prepare'); }); diff --git a/test/prepare/BasePrepare.js b/test/prepare/BasePrepare.js new file mode 100644 index 0000000..03cf2fa --- /dev/null +++ b/test/prepare/BasePrepare.js @@ -0,0 +1,158 @@ +'use strict'; + +describe('PIXI.prepare.BasePrepare', function () +{ + it('should create a new, empty, BasePrepare', function () + { + const renderer = {}; + const prep = new PIXI.prepare.BasePrepare(renderer); + + expect(prep.renderer).to.equal(renderer); + expect(prep.uploadHookHelper).to.be.null; + expect(prep.queue).to.be.empty; + expect(prep.addHooks).to.have.lengthOf(2); + expect(prep.uploadHooks).to.have.lengthOf(2); + expect(prep.completes).to.be.empty; + + prep.destroy(); + }); + + it('should add hooks', function () + { + function addHook() { /* empty */ } + function uploadHook() { /* empty */ } + const prep = new PIXI.prepare.BasePrepare(); + + prep.register(addHook, uploadHook); + + expect(prep.addHooks).to.contain(addHook); + expect(prep.addHooks).to.have.lengthOf(3); + expect(prep.uploadHooks).to.contain(uploadHook); + expect(prep.uploadHooks).to.have.lengthOf(3); + + prep.destroy(); + }); + + it('should call hooks and complete', function () + { + const prep = new PIXI.prepare.BasePrepare(); + const uploadItem = {}; + const uploadHelper = {}; + + prep.uploadHookHelper = uploadHelper; + + const addHook = sinon.spy(function (item, queue) + { + expect(item).to.equal(uploadItem); + expect(queue).to.equal(prep.queue); + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function (helper, item) + { + expect(helper).to.equal(uploadHelper); + expect(item).to.equal(uploadItem); + + return true; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload(uploadItem, complete); + + expect(prep.queue).to.contain(uploadItem); + + prep.prepareItems(); + + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should call complete if no queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + function addHook() + { + return false; + } + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook); + prep.upload({}, complete); + + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should remove un-preparable items from queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return false; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + + prep.prepareItems(); + + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should attach to SharedTicker', function (done) + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return true; + }); + + function complete() + { + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + + prep.destroy(); + + done(); + } + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + expect(addHook.called).to.be.true; + expect(uploadHook.called).to.be.false; + expect(complete.called).to.not.be.ok; + }); +}); diff --git a/test/prepare/CountLimiter.js b/test/prepare/CountLimiter.js new file mode 100644 index 0000000..570ff5c --- /dev/null +++ b/test/prepare/CountLimiter.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.prepare.CountLimiter', function () +{ + it('should limit to specified number per beginFrame()', function () + { + const limit = new PIXI.prepare.CountLimiter(3); + + limit.beginFrame(); + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.false; + + limit.beginFrame(); + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.false; + }); +}); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 8d2b5a6..d6e5a70 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -1,7 +1,5 @@ import * as core from '../../core'; - -const SharedTicker = core.ticker.shared; -const DEFAULT_UPLOADS_PER_FRAME = 4; +import BasePrepare from '../BasePrepare'; /** * The prepare manager provides functionality to upload content to the GPU. @@ -9,241 +7,25 @@ * @class * @memberof PIXI */ -export default class WebGLPrepare +export default class WebGLPrepare extends BasePrepare { /** * @param {PIXI.WebGLRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.WebGLRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; + this.uploadHookHelper = this.renderer; // Add textures and graphics to upload this.register(findBaseTextures, uploadBaseTextures) .register(findGraphics, uploadGraphics); } - /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update. - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this.renderer, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `renderer:WebGLRenderer, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** - * Destroys the plugin, don't use after this. - * - */ - destroy() - { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; - } - } /** - * The number of graphics or textures to upload to the GPU - * - * @static - * @type {number} - * @default 4 - */ -WebGLPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private @@ -255,7 +37,13 @@ { if (item instanceof core.BaseTexture) { - renderer.textureManager.updateTexture(item); + // if the texture already has a GL texture, then the texture has been prepared or rendered + // before now. If the texture changed, then the changer should be calling texture.update() which + // reuploads the texture without need for preparing it again + if (!item._glTextures[renderer.CONTEXT_UID]) + { + renderer.textureManager.updateTexture(item); + } return true; } @@ -275,7 +63,12 @@ { if (item instanceof core.Graphics) { - renderer.plugins.graphics.updateGraphics(item); + // if the item is not dirty and already has webgl data, then it got prepared or rendered + // before now and we shouldn't waste time updating it again + if (item.dirty || item.clearDirty || !item._webGL[renderer.plugins.graphics.CONTEXT_UID]) + { + renderer.plugins.graphics.updateGraphics(item); + } return true; } diff --git a/test/index.js b/test/index.js index 7424430..bf50692 100755 --- a/test/index.js +++ b/test/index.js @@ -14,4 +14,5 @@ require('./core'); require('./interaction'); require('./renders'); + require('./prepare'); }); diff --git a/test/prepare/BasePrepare.js b/test/prepare/BasePrepare.js new file mode 100644 index 0000000..03cf2fa --- /dev/null +++ b/test/prepare/BasePrepare.js @@ -0,0 +1,158 @@ +'use strict'; + +describe('PIXI.prepare.BasePrepare', function () +{ + it('should create a new, empty, BasePrepare', function () + { + const renderer = {}; + const prep = new PIXI.prepare.BasePrepare(renderer); + + expect(prep.renderer).to.equal(renderer); + expect(prep.uploadHookHelper).to.be.null; + expect(prep.queue).to.be.empty; + expect(prep.addHooks).to.have.lengthOf(2); + expect(prep.uploadHooks).to.have.lengthOf(2); + expect(prep.completes).to.be.empty; + + prep.destroy(); + }); + + it('should add hooks', function () + { + function addHook() { /* empty */ } + function uploadHook() { /* empty */ } + const prep = new PIXI.prepare.BasePrepare(); + + prep.register(addHook, uploadHook); + + expect(prep.addHooks).to.contain(addHook); + expect(prep.addHooks).to.have.lengthOf(3); + expect(prep.uploadHooks).to.contain(uploadHook); + expect(prep.uploadHooks).to.have.lengthOf(3); + + prep.destroy(); + }); + + it('should call hooks and complete', function () + { + const prep = new PIXI.prepare.BasePrepare(); + const uploadItem = {}; + const uploadHelper = {}; + + prep.uploadHookHelper = uploadHelper; + + const addHook = sinon.spy(function (item, queue) + { + expect(item).to.equal(uploadItem); + expect(queue).to.equal(prep.queue); + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function (helper, item) + { + expect(helper).to.equal(uploadHelper); + expect(item).to.equal(uploadItem); + + return true; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload(uploadItem, complete); + + expect(prep.queue).to.contain(uploadItem); + + prep.prepareItems(); + + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should call complete if no queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + function addHook() + { + return false; + } + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook); + prep.upload({}, complete); + + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should remove un-preparable items from queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return false; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + + prep.prepareItems(); + + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should attach to SharedTicker', function (done) + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return true; + }); + + function complete() + { + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + + prep.destroy(); + + done(); + } + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + expect(addHook.called).to.be.true; + expect(uploadHook.called).to.be.false; + expect(complete.called).to.not.be.ok; + }); +}); diff --git a/test/prepare/CountLimiter.js b/test/prepare/CountLimiter.js new file mode 100644 index 0000000..570ff5c --- /dev/null +++ b/test/prepare/CountLimiter.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.prepare.CountLimiter', function () +{ + it('should limit to specified number per beginFrame()', function () + { + const limit = new PIXI.prepare.CountLimiter(3); + + limit.beginFrame(); + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.false; + + limit.beginFrame(); + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.false; + }); +}); diff --git a/test/prepare/TimeLimiter.js b/test/prepare/TimeLimiter.js new file mode 100644 index 0000000..3db1f51 --- /dev/null +++ b/test/prepare/TimeLimiter.js @@ -0,0 +1,26 @@ +'use strict'; + +describe('PIXI.prepare.TimeLimiter', function () +{ + it('should limit to stop after time from beginFrame()', function (done) + { + const limit = new PIXI.prepare.TimeLimiter(3); + + limit.beginFrame(); + for (let i = 0; i < 20; ++i) + { + expect(limit.allowedToUpload()).to.be.true; + } + + setTimeout(function () + { + expect(limit.allowedToUpload()).to.be.false; + + limit.beginFrame(); + + expect(limit.allowedToUpload()).to.be.true; + + done(); + }, 4); + }); +}); diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 0b4d5fe..470d54b 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -125,10 +125,7 @@ return; } - // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px - const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; - - this._font = `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + this._font = Text.getFontStyle(style); this.context.font = this._font; @@ -142,7 +139,7 @@ // calculate text width const lineWidths = new Array(lines.length); let maxLineWidth = 0; - const fontProperties = this.determineFontProperties(this._font); + const fontProperties = Text.calculateFontProperties(this._font); for (let i = 0; i < lines.length; i++) { @@ -394,109 +391,6 @@ } /** - * Calculates the ascent, descent and fontSize of a given fontStyle - * - * @private - * @param {string} fontStyle - String representing the style of the font - * @return {Object} Font properties object - */ - determineFontProperties(fontStyle) - { - let properties = Text.fontPropertiesCache[fontStyle]; - - if (!properties) - { - properties = {}; - - const canvas = Text.fontPropertiesCanvas; - const context = Text.fontPropertiesContext; - - context.font = fontStyle; - - const width = Math.ceil(context.measureText('|MÉq').width); - let baseline = Math.ceil(context.measureText('M').width); - const height = 2 * baseline; - - baseline = baseline * 1.4 | 0; - - canvas.width = width; - canvas.height = height; - - context.fillStyle = '#f00'; - context.fillRect(0, 0, width, height); - - context.font = fontStyle; - - context.textBaseline = 'alphabetic'; - context.fillStyle = '#000'; - context.fillText('|MÉq', 0, baseline); - - const imagedata = context.getImageData(0, 0, width, height).data; - const pixels = imagedata.length; - const line = width * 4; - - let i = 0; - let idx = 0; - let stop = false; - - // ascent. scan from top to bottom until we find a non red pixel - for (i = 0; i < baseline; ++i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - if (!stop) - { - idx += line; - } - else - { - break; - } - } - - properties.ascent = baseline - i; - - idx = pixels - line; - stop = false; - - // descent. scan from bottom to top until we find a non red pixel - for (i = height; i > baseline; --i) - { - for (let j = 0; j < line; j += 4) - { - if (imagedata[idx + j] !== 255) - { - stop = true; - break; - } - } - - if (!stop) - { - idx -= line; - } - else - { - break; - } - } - - properties.descent = i - baseline; - properties.fontSize = properties.ascent + properties.descent; - - Text.fontPropertiesCache[fontStyle] = properties; - } - - return properties; - } - - /** * Applies newlines to a string to have it optimally fit into the horizontal * bounds set by the Text object's wordWrapWidth property. * @@ -819,6 +713,133 @@ this._text = text; this.dirty = true; } + + /** + * Generates a font style string to use for Text.calculateFontProperties(). Takes the same parameter + * as Text.style. + * + * @static + * @param {object|TextStyle} style - String representing the style of the font + * @return {string} Font style string, for passing to Text.calculateFontProperties() + */ + static getFontStyle(style) + { + style = style || {}; + + if (!(style instanceof TextStyle)) + { + style = new TextStyle(style); + } + + // build canvas api font setting from invididual components. Convert a numeric style.fontSize to px + const fontSizeString = (typeof style.fontSize === 'number') ? `${style.fontSize}px` : style.fontSize; + + return `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${fontSizeString} ${style.fontFamily}`; + } + + /** + * Calculates the ascent, descent and fontSize of a given fontStyle + * + * @static + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ + static calculateFontProperties(fontStyle) + { + // as this method is used for preparing assets, don't recalculate things if we don't need to + if (Text.fontPropertiesCache[fontStyle]) + { + return Text.fontPropertiesCache[fontStyle]; + } + + const properties = {}; + + const canvas = Text.fontPropertiesCanvas; + const context = Text.fontPropertiesContext; + + context.font = fontStyle; + + const width = Math.ceil(context.measureText('|MÉq').width); + let baseline = Math.ceil(context.measureText('M').width); + const height = 2 * baseline; + + baseline = baseline * 1.4 | 0; + + canvas.width = width; + canvas.height = height; + + context.fillStyle = '#f00'; + context.fillRect(0, 0, width, height); + + context.font = fontStyle; + + context.textBaseline = 'alphabetic'; + context.fillStyle = '#000'; + context.fillText('|MÉq', 0, baseline); + + const imagedata = context.getImageData(0, 0, width, height).data; + const pixels = imagedata.length; + const line = width * 4; + + let i = 0; + let idx = 0; + let stop = false; + + // ascent. scan from top to bottom until we find a non red pixel + for (i = 0; i < baseline; ++i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + if (!stop) + { + idx += line; + } + else + { + break; + } + } + + properties.ascent = baseline - i; + + idx = pixels - line; + stop = false; + + // descent. scan from bottom to top until we find a non red pixel + for (i = height; i > baseline; --i) + { + for (let j = 0; j < line; j += 4) + { + if (imagedata[idx + j] !== 255) + { + stop = true; + break; + } + } + + if (!stop) + { + idx -= line; + } + else + { + break; + } + } + + properties.descent = i - baseline; + properties.fontSize = properties.ascent + properties.descent; + + Text.fontPropertiesCache[fontStyle] = properties; + + return properties; + } } Text.fontPropertiesCache = {}; diff --git a/src/deprecation.js b/src/deprecation.js index 158a13c..130c834 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -3,6 +3,7 @@ import * as particles from './particles'; import * as extras from './extras'; import * as filters from './filters'; +import * as prepare from './prepare'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -470,6 +471,23 @@ warn('setStyle is now deprecated, please use the style property, e.g : myText.style = style;'); }; +/** + * @method + * @name PIXI.Text#determineFontProperties + * @see PIXI.Text#calculateFontProperties + * @deprecated since version 4.2.0 + * @private + * @param {string} fontStyle - String representing the style of the font + * @return {Object} Font properties object + */ +core.Text.prototype.determineFontProperties = function determineFontProperties(fontStyle) +{ + warn('determineFontProperties is now deprecated, please use the static calculateFontProperties method, ' + + 'e.g : Text.calculateFontProperties(fontStyle);'); + + return Text.calculateFontProperties(fontStyle); +}; + Object.defineProperties(core.TextStyle.prototype, { /** * Set all properties of a font as a single string @@ -662,3 +680,55 @@ return saidHello; }, }); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.canvas.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.canvas, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.CanvasPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); + +/** + * The number of graphics or textures to upload to the GPU. + * + * @name PIXI.prepare.webgl.UPLOADS_PER_FRAME + * @static + * @type {number} + * @see PIXI.prepare.BasePrepare.limiter + * @deprecated since 4.2.0 + */ +Object.defineProperty(prepare.webgl, 'UPLOADS_PER_FRAME', { + set() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please set ' + + 'renderer.plugins.prepare.limiter.maxItemsPerFrame on your renderer'); + // because we don't have a reference to the renderer, we can't actually set + // the uploads per frame, so we'll have to stick with the warning. + }, + get() + { + warn('PIXI.WebGLPrepare.UPLOADS_PER_FRAME has been removed. Please use ' + + 'renderer.plugins.prepare.limiter'); + + return NaN; + }, +}); diff --git a/src/prepare/BasePrepare.js b/src/prepare/BasePrepare.js new file mode 100644 index 0000000..66cf6af --- /dev/null +++ b/src/prepare/BasePrepare.js @@ -0,0 +1,382 @@ +import * as core from '../core'; +import CountLimiter from './limiters/CountLimiter'; +const SharedTicker = core.ticker.shared; + +const DEFAULT_UPLOADS_PER_FRAME = 4; + +/** + * The prepare manager provides functionality to upload content to the GPU. BasePrepare handles + * basic queuing functionality and is extended by {@link PIXI.prepare.WebGLPrepare} and {@link PIXI.prepare.CanvasPrepare} + * to provide preparation capabilities specific to their respective renderers. + * + * @abstract + * @class + * @memberof PIXI + */ +export default class BasePrepare +{ + /** + * @param {PIXI.SystemRenderer} renderer - A reference to the current renderer + */ + constructor(renderer) + { + /** + * The limiter to be used to control how quickly items are prepared. + * @type {PIXI.prepare.CountLimiter|PIXI.prepare.TimeLimiter} + */ + this.limiter = new CountLimiter(DEFAULT_UPLOADS_PER_FRAME); + + /** + * Reference to the renderer. + * @type {PIXI.SystemRenderer} + * @protected + */ + this.renderer = renderer; + + /** + * The only real difference between CanvasPrepare and WebGLPrepare is what they pass + * to upload hooks. That different parameter is stored here. + * @type {PIXI.prepare.CanvasPrepare|PIXI.WebGLRenderer} + * @protected + */ + this.uploadHookHelper = null; + + /** + * Collection of items to uploads at once. + * @type {Array<*>} + * @private + */ + this.queue = []; + + /** + * Collection of additional hooks for finding assets. + * @type {Array} + * @private + */ + this.addHooks = []; + + /** + * Collection of additional hooks for processing assets. + * @type {Array} + * @private + */ + this.uploadHooks = []; + + /** + * Callback to call after completed. + * @type {Array} + * @private + */ + this.completes = []; + + /** + * If prepare is ticking (running). + * @type {boolean} + * @private + */ + this.ticking = false; + + /** + * 'bound' call for prepareItems(). + * @type {Function} + * @private + */ + this.delayedTick = () => + { + // unlikely, but in case we were destroyed between tick() and delayedTick() + if (!this.queue) + { + return; + } + this.prepareItems(); + }; + + this.register(findText, drawText); + this.register(findTextStyle, calculateTextStyle); + } + + /** + * Upload all the textures and graphics to the GPU. + * + * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either + * the container or display object to search for items to upload or + * the callback function, if items have been added using `prepare.add`. + * @param {Function} [done] - Optional callback when all queued uploads have completed + */ + upload(item, done) + { + if (typeof item === 'function') + { + done = item; + item = null; + } + + // If a display object, search for items + // that we could upload + if (item) + { + this.add(item); + } + + // Get the items for upload from the display + if (this.queue.length) + { + if (done) + { + this.completes.push(done); + } + + if (!this.ticking) + { + this.ticking = true; + SharedTicker.addOnce(this.tick, this); + } + } + else if (done) + { + done(); + } + } + + /** + * Handle tick update + * + * @private + */ + tick() + { + setTimeout(this.delayedTick, 0); + } + + /** + * Actually prepare items. This is handled outside of the tick because it will take a while + * and we do NOT want to block the current animation frame from rendering. + * + * @private + */ + prepareItems() + { + this.limiter.beginFrame(); + // Upload the graphics + while (this.queue.length && this.limiter.allowedToUpload()) + { + const item = this.queue[0]; + let uploaded = false; + + for (let i = 0, len = this.uploadHooks.length; i < len; i++) + { + if (this.uploadHooks[i](this.uploadHookHelper, item)) + { + this.queue.shift(); + uploaded = true; + break; + } + } + + if (!uploaded) + { + this.queue.shift(); + } + } + + // We're finished + if (!this.queue.length) + { + this.ticking = false; + + const completes = this.completes.slice(0); + + this.completes.length = 0; + + for (let i = 0, len = completes.length; i < len; i++) + { + completes[i](); + } + } + else + { + // if we are not finished, on the next rAF do this again + SharedTicker.addOnce(this.tick, this); + } + } + + /** + * Adds hooks for finding and uploading items. + * + * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` + function must return `true` if it was able to add item to the queue. + * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and + * function must return `true` if it was able to handle upload of item. + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + register(addHook, uploadHook) + { + if (addHook) + { + this.addHooks.push(addHook); + } + + if (uploadHook) + { + this.uploadHooks.push(uploadHook); + } + + return this; + } + + /** + * Manually add an item to the uploading queue. + * + * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue + * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. + */ + add(item) + { + // Add additional hooks for finding elements on special + // types of objects that + for (let i = 0, len = this.addHooks.length; i < len; i++) + { + if (this.addHooks[i](item, this.queue)) + { + break; + } + } + + // Get childen recursively + if (item instanceof core.Container) + { + for (let i = item.children.length - 1; i >= 0; i--) + { + this.add(item.children[i]); + } + } + + return this; + } + + /** + * Destroys the plugin, don't use after this. + * + */ + destroy() + { + if (this.ticking) + { + SharedTicker.remove(this.tick, this); + } + this.ticking = false; + this.addHooks = null; + this.uploadHooks = null; + this.renderer = null; + this.completes = null; + this.queue = null; + this.limiter = null; + this.uploadHookHelper = null; + } + +} + +/** + * Built-in hook to draw PIXI.Text to its texture. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function drawText(helper, item) +{ + if (item instanceof core.Text) + { + // updating text will return early if it is not dirty + item.updateText(true); + + return true; + } + + return false; +} + +/** + * Built-in hook to calculate a text style for a PIXI.Text object. + * + * @private + * @param {PIXI.WebGLRenderer|PIXI.CanvasPrepare} helper - Not used by this upload handler + * @param {PIXI.DisplayObject} item - Item to check + * @return {boolean} If item was uploaded. + */ +function calculateTextStyle(helper, item) +{ + if (item instanceof core.TextStyle) + { + const font = core.Text.getFontStyle(item); + + if (!core.Text.fontPropertiesCache[font]) + { + core.Text.calculateFontProperties(font); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find Text objects. + * + * @private + * @param {PIXI.DisplayObject} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.Text object was found. + */ +function findText(item, queue) +{ + if (item instanceof core.Text) + { + // push the text style to prepare it - this can be really expensive + if (queue.indexOf(item.style) === -1) + { + queue.push(item.style); + } + // also push the text object so that we can render it (to canvas/texture) if needed + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + // also push the Text's texture for upload to GPU + const texture = item._texture.baseTexture; + + if (queue.indexOf(texture) === -1) + { + queue.push(texture); + } + + return true; + } + + return false; +} + +/** + * Built-in hook to find TextStyle objects. + * + * @private + * @param {PIXI.TextStyle} item - Display object to check + * @param {Array<*>} queue - Collection of items to upload + * @return {boolean} if a PIXI.TextStyle object was found. + */ +function findTextStyle(item, queue) +{ + if (item instanceof core.TextStyle) + { + if (queue.indexOf(item) === -1) + { + queue.push(item); + } + + return true; + } + + return false; +} diff --git a/src/prepare/canvas/CanvasPrepare.js b/src/prepare/canvas/CanvasPrepare.js index 51dbfd0..dfed28d 100644 --- a/src/prepare/canvas/CanvasPrepare.js +++ b/src/prepare/canvas/CanvasPrepare.js @@ -1,8 +1,7 @@ import * as core from '../../core'; -const SharedTicker = core.ticker.shared; +import BasePrepare from '../BasePrepare'; const CANVAS_START_SIZE = 16; -const DEFAULT_UPLOADS_PER_FRAME = 4; /** * The prepare manager provides functionality to upload content to the GPU @@ -13,19 +12,16 @@ * @class * @memberof PIXI */ -export default class CanvasPrepare +export default class CanvasPrepare extends BasePrepare { /** * @param {PIXI.CanvasRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.CanvasRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); + + this.uploadHookHelper = this; /** * An offline canvas to render textures to @@ -43,212 +39,17 @@ */ this.ctx = this.canvas.getContext('2d'); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; - // Add textures to upload this.register(findBaseTextures, uploadBaseTextures); } /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = CanvasPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `prepare:CanvasPrepare, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.CanvasPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** * Destroys the plugin, don't use after this. * */ destroy() { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; + super.destroy(); this.ctx = null; this.canvas = null; } @@ -256,15 +57,6 @@ } /** - * The number of graphics or textures to upload to the GPU. - * - * @static - * @type {number} - * @default 4 - */ -CanvasPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private diff --git a/src/prepare/index.js b/src/prepare/index.js index 1aa203a..f559c45 100644 --- a/src/prepare/index.js +++ b/src/prepare/index.js @@ -3,3 +3,6 @@ */ export { default as webgl } from './webgl/WebGLPrepare'; export { default as canvas } from './canvas/CanvasPrepare'; +export { default as BasePrepare } from './BasePrepare'; +export { default as CountLimiter } from './limiters/CountLimiter'; +export { default as TimeLimiter } from './limiters/TimeLimiter'; diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js new file mode 100644 index 0000000..7fd0b70 --- /dev/null +++ b/src/prepare/limiters/CountLimiter.js @@ -0,0 +1,43 @@ +/** + * CountLimiter limits the number of items handled by a {@link PIXI.prepare.BasePrepare} to a specified + * number of items per frame. + * + * @class + * @memberof PIXI + */ +export default class CountLimiter { + /** + * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. + */ + constructor(maxItemsPerFrame) + { + /** + * The maximum number of items that can be prepared each frame. + * @private + */ + this.maxItemsPerFrame = maxItemsPerFrame; + /** + * The number of items that can be prepared in the current frame. + * @type {number} + * @private + */ + this.itemsLeft = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.itemsLeft = this.maxItemsPerFrame; + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return this.itemsLeft-- > 0; + } +} diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js new file mode 100644 index 0000000..8908aba --- /dev/null +++ b/src/prepare/limiters/TimeLimiter.js @@ -0,0 +1,43 @@ +/** + * TimeLimiter limits the number of items handled by a {@link PIXI.BasePrepare} to a specified + * number of milliseconds per frame. + * + * @class + * @memberof PIXI + */ +export default class TimeLimiter { + /** + * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. + */ + constructor(maxMilliseconds) + { + /** + * The maximum milliseconds that can be spent preparing items each frame. + * @private + */ + this.maxMilliseconds = maxMilliseconds; + /** + * The start time of the current frame. + * @type {number} + * @private + */ + this.frameStart = 0; + } + + /** + * Resets any counting properties to start fresh on a new frame. + */ + beginFrame() + { + this.frameStart = Date.now(); + } + + /** + * Checks to see if another item can be uploaded. This should only be called once per item. + * @return {boolean} If the item is allowed to be uploaded. + */ + allowedToUpload() + { + return Date.now() - this.frameStart < this.maxMilliseconds; + } +} diff --git a/src/prepare/webgl/WebGLPrepare.js b/src/prepare/webgl/WebGLPrepare.js index 8d2b5a6..d6e5a70 100644 --- a/src/prepare/webgl/WebGLPrepare.js +++ b/src/prepare/webgl/WebGLPrepare.js @@ -1,7 +1,5 @@ import * as core from '../../core'; - -const SharedTicker = core.ticker.shared; -const DEFAULT_UPLOADS_PER_FRAME = 4; +import BasePrepare from '../BasePrepare'; /** * The prepare manager provides functionality to upload content to the GPU. @@ -9,241 +7,25 @@ * @class * @memberof PIXI */ -export default class WebGLPrepare +export default class WebGLPrepare extends BasePrepare { /** * @param {PIXI.WebGLRenderer} renderer - A reference to the current renderer */ constructor(renderer) { - /** - * Reference to the renderer. - * @type {PIXI.WebGLRenderer} - * @private - */ - this.renderer = renderer; + super(renderer); - /** - * Collection of items to uploads at once. - * @type {Array<*>} - * @private - */ - this.queue = []; - - /** - * Collection of additional hooks for finding assets. - * @type {Array} - * @private - */ - this.addHooks = []; - - /** - * Collection of additional hooks for processing assets. - * @type {Array} - * @private - */ - this.uploadHooks = []; - - /** - * Callback to call after completed. - * @type {Array} - * @private - */ - this.completes = []; - - /** - * If prepare is ticking (running). - * @type {boolean} - * @private - */ - this.ticking = false; + this.uploadHookHelper = this.renderer; // Add textures and graphics to upload this.register(findBaseTextures, uploadBaseTextures) .register(findGraphics, uploadGraphics); } - /** - * Upload all the textures and graphics to the GPU. - * - * @param {Function|PIXI.DisplayObject|PIXI.Container} item - Either - * the container or display object to search for items to upload or - * the callback function, if items have been added using `prepare.add`. - * @param {Function} [done] - Optional callback when all queued uploads have completed - */ - upload(item, done) - { - if (typeof item === 'function') - { - done = item; - item = null; - } - - // If a display object, search for items - // that we could upload - if (item) - { - this.add(item); - } - - // Get the items for upload from the display - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - - if (done) - { - this.completes.push(done); - } - - if (!this.ticking) - { - this.ticking = true; - SharedTicker.add(this.tick, this); - } - } - else if (done) - { - done(); - } - } - - /** - * Handle tick update. - * - * @private - */ - tick() - { - // Upload the graphics - while (this.queue.length && this.numLeft > 0) - { - const item = this.queue[0]; - let uploaded = false; - - for (let i = 0, len = this.uploadHooks.length; i < len; i++) - { - if (this.uploadHooks[i](this.renderer, item)) - { - this.numLeft--; - this.queue.shift(); - uploaded = true; - break; - } - } - - if (!uploaded) - { - this.queue.shift(); - } - } - - // We're finished - if (this.queue.length) - { - this.numLeft = WebGLPrepare.UPLOADS_PER_FRAME; - } - else - { - this.ticking = false; - - SharedTicker.remove(this.tick, this); - - const completes = this.completes.slice(0); - - this.completes.length = 0; - - for (let i = 0, len = completes.length; i < len; i++) - { - completes[i](); - } - } - } - - /** - * Adds hooks for finding and uploading items. - * - * @param {Function} [addHook] - Function call that takes two parameters: `item:*, queue:Array` - function must return `true` if it was able to add item to the queue. - * @param {Function} [uploadHook] - Function call that takes two parameters: `renderer:WebGLRenderer, item:*` and - * function must return `true` if it was able to handle upload of item. - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - register(addHook, uploadHook) - { - if (addHook) - { - this.addHooks.push(addHook); - } - - if (uploadHook) - { - this.uploadHooks.push(uploadHook); - } - - return this; - } - - /** - * Manually add an item to the uploading queue. - * - * @param {PIXI.DisplayObject|PIXI.Container|*} item - Object to add to the queue - * @return {PIXI.WebGLPrepare} Instance of plugin for chaining. - */ - add(item) - { - // Add additional hooks for finding elements on special - // types of objects that - for (let i = 0, len = this.addHooks.length; i < len; i++) - { - if (this.addHooks[i](item, this.queue)) - { - break; - } - } - - // Get childen recursively - if (item instanceof core.Container) - { - for (let i = item.children.length - 1; i >= 0; i--) - { - this.add(item.children[i]); - } - } - - return this; - } - - /** - * Destroys the plugin, don't use after this. - * - */ - destroy() - { - if (this.ticking) - { - SharedTicker.remove(this.tick, this); - } - this.ticking = false; - this.addHooks = null; - this.uploadHooks = null; - this.renderer = null; - this.completes = null; - this.queue = null; - } - } /** - * The number of graphics or textures to upload to the GPU - * - * @static - * @type {number} - * @default 4 - */ -WebGLPrepare.UPLOADS_PER_FRAME = DEFAULT_UPLOADS_PER_FRAME; - -/** * Built-in hook to upload PIXI.Texture objects to the GPU. * * @private @@ -255,7 +37,13 @@ { if (item instanceof core.BaseTexture) { - renderer.textureManager.updateTexture(item); + // if the texture already has a GL texture, then the texture has been prepared or rendered + // before now. If the texture changed, then the changer should be calling texture.update() which + // reuploads the texture without need for preparing it again + if (!item._glTextures[renderer.CONTEXT_UID]) + { + renderer.textureManager.updateTexture(item); + } return true; } @@ -275,7 +63,12 @@ { if (item instanceof core.Graphics) { - renderer.plugins.graphics.updateGraphics(item); + // if the item is not dirty and already has webgl data, then it got prepared or rendered + // before now and we shouldn't waste time updating it again + if (item.dirty || item.clearDirty || !item._webGL[renderer.plugins.graphics.CONTEXT_UID]) + { + renderer.plugins.graphics.updateGraphics(item); + } return true; } diff --git a/test/index.js b/test/index.js index 7424430..bf50692 100755 --- a/test/index.js +++ b/test/index.js @@ -14,4 +14,5 @@ require('./core'); require('./interaction'); require('./renders'); + require('./prepare'); }); diff --git a/test/prepare/BasePrepare.js b/test/prepare/BasePrepare.js new file mode 100644 index 0000000..03cf2fa --- /dev/null +++ b/test/prepare/BasePrepare.js @@ -0,0 +1,158 @@ +'use strict'; + +describe('PIXI.prepare.BasePrepare', function () +{ + it('should create a new, empty, BasePrepare', function () + { + const renderer = {}; + const prep = new PIXI.prepare.BasePrepare(renderer); + + expect(prep.renderer).to.equal(renderer); + expect(prep.uploadHookHelper).to.be.null; + expect(prep.queue).to.be.empty; + expect(prep.addHooks).to.have.lengthOf(2); + expect(prep.uploadHooks).to.have.lengthOf(2); + expect(prep.completes).to.be.empty; + + prep.destroy(); + }); + + it('should add hooks', function () + { + function addHook() { /* empty */ } + function uploadHook() { /* empty */ } + const prep = new PIXI.prepare.BasePrepare(); + + prep.register(addHook, uploadHook); + + expect(prep.addHooks).to.contain(addHook); + expect(prep.addHooks).to.have.lengthOf(3); + expect(prep.uploadHooks).to.contain(uploadHook); + expect(prep.uploadHooks).to.have.lengthOf(3); + + prep.destroy(); + }); + + it('should call hooks and complete', function () + { + const prep = new PIXI.prepare.BasePrepare(); + const uploadItem = {}; + const uploadHelper = {}; + + prep.uploadHookHelper = uploadHelper; + + const addHook = sinon.spy(function (item, queue) + { + expect(item).to.equal(uploadItem); + expect(queue).to.equal(prep.queue); + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function (helper, item) + { + expect(helper).to.equal(uploadHelper); + expect(item).to.equal(uploadItem); + + return true; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload(uploadItem, complete); + + expect(prep.queue).to.contain(uploadItem); + + prep.prepareItems(); + + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should call complete if no queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + function addHook() + { + return false; + } + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook); + prep.upload({}, complete); + + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should remove un-preparable items from queue', function () + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return false; + }); + const complete = sinon.spy(function () { /* empty */ }); + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + + prep.prepareItems(); + + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + expect(complete.calledOnce).to.be.true; + + prep.destroy(); + }); + + it('should attach to SharedTicker', function (done) + { + const prep = new PIXI.prepare.BasePrepare(); + + const addHook = sinon.spy(function (item, queue) + { + queue.push(item); + + return true; + }); + const uploadHook = sinon.spy(function () + { + return true; + }); + + function complete() + { + expect(prep.queue).to.be.empty; + expect(addHook.calledOnce).to.be.true; + expect(uploadHook.calledOnce).to.be.true; + + prep.destroy(); + + done(); + } + + prep.register(addHook, uploadHook); + prep.upload({}, complete); + + expect(prep.queue).to.have.lengthOf(1); + expect(addHook.called).to.be.true; + expect(uploadHook.called).to.be.false; + expect(complete.called).to.not.be.ok; + }); +}); diff --git a/test/prepare/CountLimiter.js b/test/prepare/CountLimiter.js new file mode 100644 index 0000000..570ff5c --- /dev/null +++ b/test/prepare/CountLimiter.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('PIXI.prepare.CountLimiter', function () +{ + it('should limit to specified number per beginFrame()', function () + { + const limit = new PIXI.prepare.CountLimiter(3); + + limit.beginFrame(); + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.false; + + limit.beginFrame(); + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.true; + expect(limit.allowedToUpload()).to.be.false; + }); +}); diff --git a/test/prepare/TimeLimiter.js b/test/prepare/TimeLimiter.js new file mode 100644 index 0000000..3db1f51 --- /dev/null +++ b/test/prepare/TimeLimiter.js @@ -0,0 +1,26 @@ +'use strict'; + +describe('PIXI.prepare.TimeLimiter', function () +{ + it('should limit to stop after time from beginFrame()', function (done) + { + const limit = new PIXI.prepare.TimeLimiter(3); + + limit.beginFrame(); + for (let i = 0; i < 20; ++i) + { + expect(limit.allowedToUpload()).to.be.true; + } + + setTimeout(function () + { + expect(limit.allowedToUpload()).to.be.false; + + limit.beginFrame(); + + expect(limit.allowedToUpload()).to.be.true; + + done(); + }, 4); + }); +}); diff --git a/test/prepare/index.js b/test/prepare/index.js new file mode 100644 index 0000000..ecc127c --- /dev/null +++ b/test/prepare/index.js @@ -0,0 +1,5 @@ +'use strict'; + +require('./CountLimiter'); +require('./TimeLimiter'); +require('./BasePrepare');