diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..25dec64 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,4 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..25dec64 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,4 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..25dec64 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,4 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..25dec64 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,4 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/src/core/index.js b/src/core/index.js index 90155f9..f03fe15 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -30,6 +30,7 @@ export { default as GraphicsData } from './graphics/GraphicsData'; export { default as GraphicsRenderer } from './graphics/webgl/GraphicsRenderer'; export { default as CanvasGraphicsRenderer } from './graphics/canvas/CanvasGraphicsRenderer'; +export { default as Spritesheet } from './textures/Spritesheet'; export { default as Texture } from './textures/Texture'; export { default as BaseTexture } from './textures/BaseTexture'; export { default as RenderTexture } from './textures/RenderTexture'; diff --git a/src/core/textures/Spritesheet.js b/src/core/textures/Spritesheet.js new file mode 100644 index 0000000..d469c62 --- /dev/null +++ b/src/core/textures/Spritesheet.js @@ -0,0 +1,274 @@ +import { Rectangle, Texture } from '../'; +import { getResolutionOfUrl, TextureCache } from '../utils'; + +/** + * Utility class for maintaining reference to a collection + * of Textures on a single Spritesheet. + * + * @class + * @memberof PIXI + */ +export default class Spritesheet +{ + /** + * The maximum number of Textures to build per process. + * + * @type {number} + * @default 1000 + */ + static get BATCH_SIZE() + { + return 1000; + } + + /** + * @param {PIXI.BaseTexture} baseTexture Reference to the source BaseTexture object. + * @param {Object} data - Spritesheet image data. + * @param {string} [resolutionFilename] - The filename to consider when determining + * the resolution of the spritesheet. If not provided, the imageUrl will + * be used on the BaseTexture. + */ + constructor(baseTexture, data, resolutionFilename = null) + { + /** + * Reference to ths source texture + * @type {PIXI.BaseTexture} + */ + this.baseTexture = baseTexture; + + /** + * Map of spritesheet textures. + * @type {Object} + */ + this.textures = {}; + + /** + * Reference to the original JSON data. + * @type {Object} + */ + this.data = data; + + /** + * The resolution of the spritesheet. + * @type {number} + */ + this.resolution = this._updateResolution( + resolutionFilename || this.baseTexture.imageUrl + ); + + /** + * Map of spritesheet frames. + * @type {Object} + * @private + */ + this._frames = this.data.frames; + + /** + * Collection of frame names. + * @type {string[]} + * @private + */ + this._frameKeys = Object.keys(this._frames); + + /** + * Current batch index being processed. + * @type {number} + * @private + */ + this._batchIndex = 0; + + /** + * Callback when parse is completed. + * @type {Function} + * @private + */ + this._callback = null; + } + + /** + * Generate the resolution from the filename or fallback + * to the meta.scale field of the JSON data. + * + * @private + * @param {string} resolutionFilename - The filename to use for resolving + * the default resolution. + * @return {number} Resolution to use for spritesheet. + */ + _updateResolution(resolutionFilename) + { + const scale = this.data.meta.scale; + + // Use a defaultValue of `null` to check if a url-based resolution is set + let resolution = getResolutionOfUrl(resolutionFilename, null); + + // No resolution found via URL + if (resolution === null) + { + // Use the scale value or default to 1 + resolution = scale !== undefined ? parseFloat(scale) : 1; + } + + // For non-1 resolutions, update baseTexture + if (resolution !== 1) + { + this.baseTexture.resolution = resolution; + this.baseTexture.update(); + } + + return resolution; + } + + /** + * Parser spritesheet from loaded data. This is done asynchronously + * to prevent creating too many Texture within a single process. + * + * @param {Function} callback - Callback when complete returns + * a map of the Textures for this spritesheet. + */ + parse(callback) + { + this._batchIndex = 0; + this._callback = callback; + + if (this._frameKeys.length <= Spritesheet.BATCH_SIZE) + { + this._processFrames(0); + this._parseComplete(); + } + else + { + this._nextBatch(); + } + } + + /** + * Process a batch of frames + * + * @private + * @param {number} initialFrameIndex - The index of frame to start. + */ + _processFrames(initialFrameIndex) + { + let frameIndex = initialFrameIndex; + const maxFrames = Spritesheet.BATCH_SIZE; + + while (frameIndex - initialFrameIndex < maxFrames && frameIndex < this._frameKeys.length) + { + const i = this._frameKeys[frameIndex]; + const rect = this._frames[i].frame; + + if (rect) + { + let frame = null; + let trim = null; + const orig = new Rectangle( + 0, + 0, + this._frames[i].sourceSize.w / this.resolution, + this._frames[i].sourceSize.h / this.resolution + ); + + if (this._frames[i].rotated) + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.h / this.resolution, + rect.w / this.resolution + ); + } + else + { + frame = new Rectangle( + rect.x / this.resolution, + rect.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + // Check to see if the sprite is trimmed + if (this._frames[i].trimmed) + { + trim = new Rectangle( + this._frames[i].spriteSourceSize.x / this.resolution, + this._frames[i].spriteSourceSize.y / this.resolution, + rect.w / this.resolution, + rect.h / this.resolution + ); + } + + this.textures[i] = new Texture( + this.baseTexture, + frame, + orig, + trim, + this._frames[i].rotated ? 2 : 0 + ); + + // lets also add the frame to pixi's global cache for fromFrame and fromImage functions + TextureCache[i] = this.textures[i]; + } + + frameIndex++; + } + } + + /** + * The parse has completed. + * + * @private + */ + _parseComplete() + { + const callback = this._callback; + + this._callback = null; + this._batchIndex = 0; + callback.call(this, this.textures); + } + + /** + * Begin the next batch of textures. + * + * @private + */ + _nextBatch() + { + this._processFrames(this._batchIndex * Spritesheet.BATCH_SIZE); + this._batchIndex++; + setTimeout(() => + { + if (this._batchIndex * Spritesheet.BATCH_SIZE < this._frameKeys.length) + { + this._nextBatch(); + } + else + { + this._parseComplete(); + } + }, 0); + } + + /** + * Destroy Spritesheet and don't use after this. + * + * @param {boolean} [destroyBase=false] Whether to destroy the base texture as well + */ + destroy(destroyBase = false) + { + for (const i in this.textures) + { + this.textures[i].destroy(); + } + this._frames = null; + this._frameKeys = null; + this.data = null; + this.textures = null; + if (destroyBase) + { + this.baseTexture.destroy(); + } + this.baseTexture = null; + } +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index 6403d4f..d510075 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -3,7 +3,7 @@ import TextureUvs from './TextureUvs'; import EventEmitter from 'eventemitter3'; import { Rectangle } from '../math'; -import { TextureCache, BaseTextureCache } from '../utils'; +import { TextureCache, BaseTextureCache, getResolutionOfUrl } from '../utils'; /** * A texture stores the information that represents an image or part of an image. It cannot be added @@ -413,6 +413,43 @@ } /** + * Create a texture from a source and add to the cache. + * + * @static + * @param {HTMLImageElement|HTMLCanvasElement} source - The input source. + * @param {String} imageUrl - File name of texture, for cache and resolving resolution. + * @param {String} [name] - Human readible name for the texture cache. If no name is + * specified, only `imageUrl` will be used as the cache ID. + * @return {PIXI.Texture} Output texture + */ + static fromLoader(source, imageUrl, name) + { + const baseTexture = new BaseTexture(source, null, getResolutionOfUrl(imageUrl)); + const texture = new Texture(baseTexture); + + baseTexture.imageUrl = imageUrl; + + // No name, use imageUrl instead + if (!name) + { + name = imageUrl; + } + + // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions + BaseTextureCache[name] = baseTexture; + TextureCache[name] = texture; + + // also add references by url if they are different. + if (name !== imageUrl) + { + BaseTextureCache[imageUrl] = baseTexture; + TextureCache[imageUrl] = texture; + } + + return texture; + } + + /** * Adds a texture to the global TextureCache. This cache is shared across the whole PIXI object. * * @static diff --git a/src/extras/BitmapText.js b/src/extras/BitmapText.js index fb6c343..0a0e150 100644 --- a/src/extras/BitmapText.js +++ b/src/extras/BitmapText.js @@ -455,6 +455,73 @@ return this._textHeight; } + + /** + * Register a bitmap font with data and a texture. + * + * @static + * @param {XMLDocument} xml - The XML document data. + * @param {PIXI.Texture} texture - Texture with all symbols. + * @return {Object} Result font object with font, size, lineHeight and char fields. + */ + static registerFont(xml, texture) + { + const data = {}; + const info = xml.getElementsByTagName('info')[0]; + const common = xml.getElementsByTagName('common')[0]; + + data.font = info.getAttribute('face'); + data.size = parseInt(info.getAttribute('size'), 10); + data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); + data.chars = {}; + + // parse letters + const letters = xml.getElementsByTagName('char'); + + for (let i = 0; i < letters.length; i++) + { + const letter = letters[i]; + const charCode = parseInt(letter.getAttribute('id'), 10); + + const textureRect = new core.Rectangle( + parseInt(letter.getAttribute('x'), 10) + texture.frame.x, + parseInt(letter.getAttribute('y'), 10) + texture.frame.y, + parseInt(letter.getAttribute('width'), 10), + parseInt(letter.getAttribute('height'), 10) + ); + + data.chars[charCode] = { + xOffset: parseInt(letter.getAttribute('xoffset'), 10), + yOffset: parseInt(letter.getAttribute('yoffset'), 10), + xAdvance: parseInt(letter.getAttribute('xadvance'), 10), + kerning: {}, + texture: new core.Texture(texture.baseTexture, textureRect), + + }; + } + + // parse kernings + const kernings = xml.getElementsByTagName('kerning'); + + for (let i = 0; i < kernings.length; i++) + { + const kerning = kernings[i]; + const first = parseInt(kerning.getAttribute('first'), 10); + const second = parseInt(kerning.getAttribute('second'), 10); + const amount = parseInt(kerning.getAttribute('amount'), 10); + + if (data.chars[second]) + { + data.chars[second].kerning[first] = amount; + } + } + + // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 + // but it's very likely to change + BitmapText.fonts[data.font] = data; + + return data; + } } BitmapText.fonts = {}; diff --git a/src/loaders/bitmapFontParser.js b/src/loaders/bitmapFontParser.js index cc2618c..f4cf996 100644 --- a/src/loaders/bitmapFontParser.js +++ b/src/loaders/bitmapFontParser.js @@ -1,63 +1,19 @@ import * as path from 'path'; -import { Rectangle, Texture, utils } from '../core'; +import { utils } from '../core'; import { Resource } from 'resource-loader'; import { BitmapText } from '../extras'; +/** + * Register a BitmapText font from loader resource. + * + * @function parseBitmapFontData + * @memberof PIXI.loaders + * @param {PIXI.loaders.Resource} resource - Loader resource. + * @param {PIXI.Texture} texture - Reference to texture. + */ export function parse(resource, texture) { - const data = {}; - const info = resource.data.getElementsByTagName('info')[0]; - const common = resource.data.getElementsByTagName('common')[0]; - - data.font = info.getAttribute('face'); - data.size = parseInt(info.getAttribute('size'), 10); - data.lineHeight = parseInt(common.getAttribute('lineHeight'), 10); - data.chars = {}; - - // parse letters - const letters = resource.data.getElementsByTagName('char'); - - for (let i = 0; i < letters.length; i++) - { - const charCode = parseInt(letters[i].getAttribute('id'), 10); - - const textureRect = new Rectangle( - parseInt(letters[i].getAttribute('x'), 10) + texture.frame.x, - parseInt(letters[i].getAttribute('y'), 10) + texture.frame.y, - parseInt(letters[i].getAttribute('width'), 10), - parseInt(letters[i].getAttribute('height'), 10) - ); - - data.chars[charCode] = { - xOffset: parseInt(letters[i].getAttribute('xoffset'), 10), - yOffset: parseInt(letters[i].getAttribute('yoffset'), 10), - xAdvance: parseInt(letters[i].getAttribute('xadvance'), 10), - kerning: {}, - texture: new Texture(texture.baseTexture, textureRect), - - }; - } - - // parse kernings - const kernings = resource.data.getElementsByTagName('kerning'); - - for (let i = 0; i < kernings.length; i++) - { - const first = parseInt(kernings[i].getAttribute('first'), 10); - const second = parseInt(kernings[i].getAttribute('second'), 10); - const amount = parseInt(kernings[i].getAttribute('amount'), 10); - - if (data.chars[second]) - { - data.chars[second].kerning[first] = amount; - } - } - - resource.bitmapFont = data; - - // I'm leaving this as a temporary fix so we can test the bitmap fonts in v3 - // but it's very likely to change - BitmapText.fonts[data.font] = data; + resource.bitmapFont = BitmapText.registerFont(resource.data, texture); } export default function () diff --git a/src/loaders/index.js b/src/loaders/index.js index 8942436..5189793 100644 --- a/src/loaders/index.js +++ b/src/loaders/index.js @@ -5,4 +5,11 @@ export { default as bitmapFontParser, parse as parseBitmapFontData } from './bitmapFontParser'; export { default as spritesheetParser } from './spritesheetParser'; export { default as textureParser } from './textureParser'; + +/** + * Reference to **resource-loader**'s Resource class. + * See https://github.com/englercj/resource-loader + * @class Resource + * @memberof PIXI.loaders + */ export { Resource } from 'resource-loader'; diff --git a/src/loaders/spritesheetParser.js b/src/loaders/spritesheetParser.js index 7570332..e57dfb2 100644 --- a/src/loaders/spritesheetParser.js +++ b/src/loaders/spritesheetParser.js @@ -1,8 +1,6 @@ import { Resource } from 'resource-loader'; import path from 'path'; -import * as core from '../core'; - -const BATCH_SIZE = 1000; +import { Spritesheet } from '../core'; export default function () { @@ -43,134 +41,18 @@ // load the image for this sheet this.add(imageResourceName, resourcePath, loadOptions, function onImageLoad(res) { - resource.textures = {}; + const spritesheet = new Spritesheet( + res.texture.baseTexture, + resource.data, + resource.url + ); - const frames = resource.data.frames; - const frameKeys = Object.keys(frames); - const baseTexture = res.texture.baseTexture; - const scale = resource.data.meta.scale; - - // Use a defaultValue of `null` to check if a url-based resolution is set - let resolution = core.utils.getResolutionOfUrl(resource.url, null); - - // No resolution found via URL - if (resolution === null) + spritesheet.parse(() => { - // Use the scale value or default to 1 - resolution = scale !== undefined ? scale : 1; - } - - // For non-1 resolutions, update baseTexture - if (resolution !== 1) - { - baseTexture.resolution = resolution; - baseTexture.update(); - } - - let batchIndex = 0; - - function processFrames(initialFrameIndex, maxFrames) - { - let frameIndex = initialFrameIndex; - - while (frameIndex - initialFrameIndex < maxFrames && frameIndex < frameKeys.length) - { - const i = frameKeys[frameIndex]; - const rect = frames[i].frame; - - if (rect) - { - let frame = null; - let trim = null; - const orig = new core.Rectangle( - 0, - 0, - frames[i].sourceSize.w / resolution, - frames[i].sourceSize.h / resolution - ); - - if (frames[i].rotated) - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.h / resolution, - rect.w / resolution - ); - } - else - { - frame = new core.Rectangle( - rect.x / resolution, - rect.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - // Check to see if the sprite is trimmed - if (frames[i].trimmed) - { - trim = new core.Rectangle( - frames[i].spriteSourceSize.x / resolution, - frames[i].spriteSourceSize.y / resolution, - rect.w / resolution, - rect.h / resolution - ); - } - - resource.textures[i] = new core.Texture( - baseTexture, - frame, - orig, - trim, - frames[i].rotated ? 2 : 0 - ); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage functions - core.utils.TextureCache[i] = resource.textures[i]; - } - - frameIndex++; - } - } - - function shouldProcessNextBatch() - { - return batchIndex * BATCH_SIZE < frameKeys.length; - } - - function processNextBatch(done) - { - processFrames(batchIndex * BATCH_SIZE, BATCH_SIZE); - batchIndex++; - setTimeout(done, 0); - } - - function iteration() - { - processNextBatch(() => - { - if (shouldProcessNextBatch()) - { - iteration(); - } - else - { - next(); - } - }); - } - - if (frameKeys.length <= BATCH_SIZE) - { - processFrames(0, BATCH_SIZE); + resource.spritesheet = spritesheet; + resource.textures = spritesheet.textures; next(); - } - else - { - iteration(); - } + }); }); }; } diff --git a/src/loaders/textureParser.js b/src/loaders/textureParser.js index 5398a7f..480edc1 100644 --- a/src/loaders/textureParser.js +++ b/src/loaders/textureParser.js @@ -1,5 +1,5 @@ -import * as core from '../core'; import { Resource } from 'resource-loader'; +import Texture from '../core/textures/Texture'; export default function () { @@ -8,23 +8,12 @@ // create a new texture if the data is an Image object if (resource.data && resource.type === Resource.TYPE.IMAGE) { - const baseTexture = new core.BaseTexture(resource.data, null, core.utils.getResolutionOfUrl(resource.url)); - - baseTexture.imageUrl = resource.url; - resource.texture = new core.Texture(baseTexture); - - // lets also add the frame to pixi's global cache for fromFrame and fromImage fucntions - core.utils.BaseTextureCache[resource.name] = baseTexture; - core.utils.TextureCache[resource.name] = resource.texture; - - // also add references by url if they are different. - if (resource.name !== resource.url) - { - core.utils.BaseTextureCache[resource.url] = baseTexture; - core.utils.TextureCache[resource.url] = resource.texture; - } + resource.texture = Texture.fromLoader( + resource.data, + resource.url, + resource.name + ); } - next(); }; } diff --git a/test/core/Spritesheet.js b/test/core/Spritesheet.js new file mode 100644 index 0000000..a6dad6b --- /dev/null +++ b/test/core/Spritesheet.js @@ -0,0 +1,85 @@ +'use strict'; + +const path = require('path'); + +describe('PIXI.Spritesheet', function () +{ + before(function () + { + this.resources = path.join(__dirname, 'resources'); + this.validate = function (spritesheet, done) + { + spritesheet.parse(function (textures) + { + const id = 'goldmine_10_5.png'; + + expect(Object.keys(textures).length).to.equal(1); + expect(Object.keys(spritesheet.textures).length).to.equal(1); + expect(textures[id]).to.be.an.instanceof(PIXI.Texture); + expect(textures[id].width).to.equal(spritesheet.data.frames[id].frame.w / spritesheet.resolution); + expect(textures[id].height).to.equal(spritesheet.data.frames[id].frame.h / spritesheet.resolution); + spritesheet.destroy(true); + expect(spritesheet.textures).to.be.null; + expect(spritesheet.baseTexture).to.be.null; + done(); + }); + }; + }); + + it('should exist on PIXI', function () + { + expect(PIXI.Spritesheet).to.be.a.function; + expect(PIXI.Spritesheet.BATCH_SIZE).to.be.a.number; + }); + + it('should create an instance', function () + { + const baseTexture = new PIXI.BaseTexture(); + const data = { + frames: {}, + meta: {}, + }; + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(spritesheet.data).to.equal(data); + expect(spritesheet.baseTexture).to.equal(baseTexture); + expect(spritesheet.resolution).to.equal(1); + + spritesheet.destroy(true); + }); + + it('should create instance with scale resolution', function (done) + { + const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1.png'); + expect(spritesheet.resolution).to.equal(0.5); + + this.validate(spritesheet, done); + }); + + it('should create instance with filename resolution', function (done) + { + const uri = path.resolve(this.resources, 'building1@2x.json'); + const data = require(uri); // eslint-disable-line global-require + const image = new Image(); + + image.src = path.join(this.resources, data.meta.image); + + const baseTexture = new PIXI.BaseTexture(image, null, 1); + const spritesheet = new PIXI.Spritesheet(baseTexture, data, uri); + + expect(data).to.be.an.object; + expect(data.meta.image).to.equal('building1@2x.png'); + expect(spritesheet.resolution).to.equal(2); + + this.validate(spritesheet, done); + }); +}); diff --git a/test/core/Texture.js b/test/core/Texture.js new file mode 100644 index 0000000..8c4ef98 --- /dev/null +++ b/test/core/Texture.js @@ -0,0 +1,19 @@ +'use strict'; + +describe('PIXI.Texture', function () +{ + it('should register Texture from Loader', function () + { + const URL = 'foo.png'; + const NAME = 'bar'; + const image = new Image(); + + const texture = PIXI.Texture.fromLoader(image, URL, NAME); + + expect(texture.baseTexture.imageUrl).to.equal('foo.png'); + expect(PIXI.utils.TextureCache[NAME]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[NAME]).to.equal(texture.baseTexture); + expect(PIXI.utils.TextureCache[URL]).to.equal(texture); + expect(PIXI.utils.BaseTextureCache[URL]).to.equal(texture.baseTexture); + }); +}); diff --git a/test/core/index.js b/test/core/index.js index 63a8f21..25dec64 100755 --- a/test/core/index.js +++ b/test/core/index.js @@ -7,6 +7,7 @@ require('./DisplayObject'); require('./getLocalBounds'); require('./Sprite'); +require('./Spritesheet'); require('./TilingSprite'); require('./TextStyle'); require('./Text'); @@ -25,3 +26,4 @@ require('./SpriteRenderer'); require('./WebGLRenderer'); require('./Ellipse'); +require('./Texture'); diff --git a/test/core/resources/building1.json b/test/core/resources/building1.json new file mode 100755 index 0000000..03fa5c1 --- /dev/null +++ b/test/core/resources/building1.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":95,"h":115}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":95,"h":115}, + "sourceSize": {"w":95,"h":115}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1.png", + "format": "RGBA8888", + "size": {"w":128,"h":128}, + "scale": "0.5", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1.png b/test/core/resources/building1.png new file mode 100755 index 0000000..7e1e114 --- /dev/null +++ b/test/core/resources/building1.png Binary files differ diff --git a/test/core/resources/building1@2x.json b/test/core/resources/building1@2x.json new file mode 100755 index 0000000..24e25ff --- /dev/null +++ b/test/core/resources/building1@2x.json @@ -0,0 +1,21 @@ +{"frames": { + +"goldmine_10_5.png": +{ + "frame": {"x":1,"y":1,"w":190,"h":229}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":190,"h":229}, + "sourceSize": {"w":190,"h":229}, + "pivot": {"x":0.5,"y":0.5} +}}, +"meta": { + "app": "http://www.codeandweb.com/texturepacker", + "version": "1.0", + "image": "building1@2x.png", + "format": "RGBA8888", + "size": {"w":256,"h":256}, + "scale": "1", + "smartupdate": "$TexturePacker:SmartUpdate:d7a5e54c8f8a3fecd508baf190c44807:99a0e3a4dc0f441e4aad77614191ab38:6046b8eb706ddefaa771c33ceb7cb6d5$" +} +} diff --git a/test/core/resources/building1@2x.png b/test/core/resources/building1@2x.png new file mode 100755 index 0000000..d5ecd04 --- /dev/null +++ b/test/core/resources/building1@2x.png Binary files differ