diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/packages/polyfill/src/index.js b/packages/polyfill/src/index.js index d963dee..fb2ca08 100644 --- a/packages/polyfill/src/index.js +++ b/packages/polyfill/src/index.js @@ -1,3 +1,4 @@ +import './Promise'; import './Object.assign'; import './requestAnimationFrame'; import './Math.sign'; @@ -21,3 +22,13 @@ { window.Uint16Array = Array; } + +if (!window.Uint8Array) +{ + window.Uint8Array = Array; +} + +if (!window.Int32Array) +{ + window.Int32Array = Array; +} diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/packages/polyfill/src/index.js b/packages/polyfill/src/index.js index d963dee..fb2ca08 100644 --- a/packages/polyfill/src/index.js +++ b/packages/polyfill/src/index.js @@ -1,3 +1,4 @@ +import './Promise'; import './Object.assign'; import './requestAnimationFrame'; import './Math.sign'; @@ -21,3 +22,13 @@ { window.Uint16Array = Array; } + +if (!window.Uint8Array) +{ + window.Uint8Array = Array; +} + +if (!window.Int32Array) +{ + window.Int32Array = Array; +} diff --git a/packages/settings/src/settings.js b/packages/settings/src/settings.js index 4de6795..3f4132e 100644 --- a/packages/settings/src/settings.js +++ b/packages/settings/src/settings.js @@ -189,4 +189,15 @@ * @type {boolean} */ CAN_UPLOAD_SAME_BUFFER: canUploadSameBuffer(), + + /** + * Enables bitmap creation before image load + * + * @static + * @constant + * @memberof PIXI + * @type {boolean} + * @default true + */ + CREATE_IMAGE_BITMAP: true, }; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/packages/polyfill/src/index.js b/packages/polyfill/src/index.js index d963dee..fb2ca08 100644 --- a/packages/polyfill/src/index.js +++ b/packages/polyfill/src/index.js @@ -1,3 +1,4 @@ +import './Promise'; import './Object.assign'; import './requestAnimationFrame'; import './Math.sign'; @@ -21,3 +22,13 @@ { window.Uint16Array = Array; } + +if (!window.Uint8Array) +{ + window.Uint8Array = Array; +} + +if (!window.Int32Array) +{ + window.Int32Array = Array; +} diff --git a/packages/settings/src/settings.js b/packages/settings/src/settings.js index 4de6795..3f4132e 100644 --- a/packages/settings/src/settings.js +++ b/packages/settings/src/settings.js @@ -189,4 +189,15 @@ * @type {boolean} */ CAN_UPLOAD_SAME_BUFFER: canUploadSameBuffer(), + + /** + * Enables bitmap creation before image load + * + * @static + * @constant + * @memberof PIXI + * @type {boolean} + * @default true + */ + CREATE_IMAGE_BITMAP: true, }; diff --git a/packages/spritesheet/src/Spritesheet.js b/packages/spritesheet/src/Spritesheet.js index c314691..4f66a77 100644 --- a/packages/spritesheet/src/Spritesheet.js +++ b/packages/spritesheet/src/Spritesheet.js @@ -113,8 +113,7 @@ // For non-1 resolutions, update baseTexture if (resolution !== 1) { - this.baseTexture.resolution = resolution; - this.baseTexture.updateResolution(); + this.baseTexture.setResolution(resolution); } return resolution; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/packages/polyfill/src/index.js b/packages/polyfill/src/index.js index d963dee..fb2ca08 100644 --- a/packages/polyfill/src/index.js +++ b/packages/polyfill/src/index.js @@ -1,3 +1,4 @@ +import './Promise'; import './Object.assign'; import './requestAnimationFrame'; import './Math.sign'; @@ -21,3 +22,13 @@ { window.Uint16Array = Array; } + +if (!window.Uint8Array) +{ + window.Uint8Array = Array; +} + +if (!window.Int32Array) +{ + window.Int32Array = Array; +} diff --git a/packages/settings/src/settings.js b/packages/settings/src/settings.js index 4de6795..3f4132e 100644 --- a/packages/settings/src/settings.js +++ b/packages/settings/src/settings.js @@ -189,4 +189,15 @@ * @type {boolean} */ CAN_UPLOAD_SAME_BUFFER: canUploadSameBuffer(), + + /** + * Enables bitmap creation before image load + * + * @static + * @constant + * @memberof PIXI + * @type {boolean} + * @default true + */ + CREATE_IMAGE_BITMAP: true, }; diff --git a/packages/spritesheet/src/Spritesheet.js b/packages/spritesheet/src/Spritesheet.js index c314691..4f66a77 100644 --- a/packages/spritesheet/src/Spritesheet.js +++ b/packages/spritesheet/src/Spritesheet.js @@ -113,8 +113,7 @@ // For non-1 resolutions, update baseTexture if (resolution !== 1) { - this.baseTexture.resolution = resolution; - this.baseTexture.updateResolution(); + this.baseTexture.setResolution(resolution); } return resolution; diff --git a/packages/spritesheet/test/Spritesheet.js b/packages/spritesheet/test/Spritesheet.js index 35d8688..17ea65f 100644 --- a/packages/spritesheet/test/Spritesheet.js +++ b/packages/spritesheet/test/Spritesheet.js @@ -73,7 +73,7 @@ it('should create instance with BaseTexture source scale', function (done) { const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require - const baseTexture = new BaseTexture.fromImage(data.meta.image, undefined, undefined, 1.5); + const baseTexture = BaseTexture.from(data.meta.image);// , undefined, undefined, 1.5); const spritesheet = new Spritesheet(baseTexture, data); expect(data).to.be.an.object; @@ -92,7 +92,7 @@ image.src = path.join(this.resources, data.meta.image); image.onload = () => { - const baseTexture = new BaseTexture(image, null, 1); + const baseTexture = new BaseTexture(image, { resolution: 1 }); const spritesheet = new Spritesheet(baseTexture, data, uri); expect(data).to.be.an.object; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/packages/polyfill/src/index.js b/packages/polyfill/src/index.js index d963dee..fb2ca08 100644 --- a/packages/polyfill/src/index.js +++ b/packages/polyfill/src/index.js @@ -1,3 +1,4 @@ +import './Promise'; import './Object.assign'; import './requestAnimationFrame'; import './Math.sign'; @@ -21,3 +22,13 @@ { window.Uint16Array = Array; } + +if (!window.Uint8Array) +{ + window.Uint8Array = Array; +} + +if (!window.Int32Array) +{ + window.Int32Array = Array; +} diff --git a/packages/settings/src/settings.js b/packages/settings/src/settings.js index 4de6795..3f4132e 100644 --- a/packages/settings/src/settings.js +++ b/packages/settings/src/settings.js @@ -189,4 +189,15 @@ * @type {boolean} */ CAN_UPLOAD_SAME_BUFFER: canUploadSameBuffer(), + + /** + * Enables bitmap creation before image load + * + * @static + * @constant + * @memberof PIXI + * @type {boolean} + * @default true + */ + CREATE_IMAGE_BITMAP: true, }; diff --git a/packages/spritesheet/src/Spritesheet.js b/packages/spritesheet/src/Spritesheet.js index c314691..4f66a77 100644 --- a/packages/spritesheet/src/Spritesheet.js +++ b/packages/spritesheet/src/Spritesheet.js @@ -113,8 +113,7 @@ // For non-1 resolutions, update baseTexture if (resolution !== 1) { - this.baseTexture.resolution = resolution; - this.baseTexture.updateResolution(); + this.baseTexture.setResolution(resolution); } return resolution; diff --git a/packages/spritesheet/test/Spritesheet.js b/packages/spritesheet/test/Spritesheet.js index 35d8688..17ea65f 100644 --- a/packages/spritesheet/test/Spritesheet.js +++ b/packages/spritesheet/test/Spritesheet.js @@ -73,7 +73,7 @@ it('should create instance with BaseTexture source scale', function (done) { const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require - const baseTexture = new BaseTexture.fromImage(data.meta.image, undefined, undefined, 1.5); + const baseTexture = BaseTexture.from(data.meta.image);// , undefined, undefined, 1.5); const spritesheet = new Spritesheet(baseTexture, data); expect(data).to.be.an.object; @@ -92,7 +92,7 @@ image.src = path.join(this.resources, data.meta.image); image.onload = () => { - const baseTexture = new BaseTexture(image, null, 1); + const baseTexture = new BaseTexture(image, { resolution: 1 }); const spritesheet = new Spritesheet(baseTexture, data, uri); expect(data).to.be.an.object; diff --git a/packages/text-bitmap/test/BitmapFontLoader.js b/packages/text-bitmap/test/BitmapFontLoader.js index f0a6db5..8cb514e 100644 --- a/packages/text-bitmap/test/BitmapFontLoader.js +++ b/packages/text-bitmap/test/BitmapFontLoader.js @@ -169,7 +169,11 @@ it('should properly register SCALED bitmap font', function (done) { - const texture = new Texture(new BaseTexture(this.fontScaledImage, null, 0.5)); + const baseTexture = new BaseTexture(this.fontScaledImage); + + baseTexture.setResolution(0.5); + + const texture = new Texture(baseTexture); const font = BitmapText.registerFont(this.fontScaledXML, texture); expect(font).to.be.an.object; diff --git a/bundles/pixi.js/src/deprecated.js b/bundles/pixi.js/src/deprecated.js index a390a1a..dd1acbd 100644 --- a/bundles/pixi.js/src/deprecated.js +++ b/bundles/pixi.js/src/deprecated.js @@ -609,4 +609,52 @@ }, }, }); + + const { BaseTexture } = PIXI; + + /** + * @method fromImage + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromImage = function fromImage(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromImage has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; + + /** + * @method fromCanvas + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromCanvas = function fromCanvas(canvas, scaleMode) + { + warn('PIXI.BaseTexture.fromCanvas has been replaced with PIXI.BaseTexture.from'); + + return BaseTexture.from(canvas, { scaleMode }); + }; + + /** + * @method fromSVG + * @static + * @memberof PIXI.BaseTexture + * @deprecated since 5.0.0 + * @see PIXI.BaseTexture.from + */ + BaseTexture.fromSVG = function fromSVG(canvas, crossorigin, scaleMode, scale) + { + warn('PIXI.BaseTexture.fromSVG has been replaced with PIXI.BaseTexture.from'); + + const resourceOptions = { scale, crossorigin }; + + return BaseTexture.from(canvas, { scaleMode, resourceOptions }); + }; } diff --git a/packages/constants/src/index.js b/packages/constants/src/index.js index 0b4e175..1f306bd 100644 --- a/packages/constants/src/index.js +++ b/packages/constants/src/index.js @@ -95,6 +95,22 @@ TRIANGLE_FAN: 6, }; +/** + * Various GL texture/resources formats. + * + * @static + * @constant + * @memberof PIXI + * @name FORMATS + * @type {object} + * @property {number} RGBA=6408 + * @property {number} RGB=6407 + * @property {number} ALPHA=6406 + * @property {number} LUMINANCE=6409 + * @property {number} LUMINANCE_ALPHA=6410 + * @property {number} DEPTH_COMPONENT=6402 + * @property {number} DEPTH_STENCIL=34041 + */ export const FORMATS = { RGBA: 6408, RGB: 6407, @@ -105,6 +121,24 @@ DEPTH_STENCIL: 34041, }; +/** + * Various GL target types. + * + * @static + * @constant + * @memberof PIXI + * @name TARGETS + * @type {object} + * @property {number} TEXTURE_2D=3553 + * @property {number} TEXTURE_CUBE_MAP=34067 + * @property {number} TEXTURE_2D_ARRAY=35866 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_X=34069 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_X=34070 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Y=34071 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Y=34072 + * @property {number} TEXTURE_CUBE_MAP_POSITIVE_Z=34073 + * @property {number} TEXTURE_CUBE_MAP_NEGATIVE_Z=34074 + */ export const TARGETS = { TEXTURE_2D: 3553, TEXTURE_CUBE_MAP: 34067, @@ -117,6 +151,22 @@ TEXTURE_CUBE_MAP_NEGATIVE_Z: 34074, }; +/** + * Various GL data format types. + * + * @static + * @constant + * @memberof PIXI + * @name TYPES + * @type {object} + * @property {number} UNSIGNED_BYTE=5121 + * @property {number} UNSIGNED_SHORT=5123 + * @property {number} UNSIGNED_SHORT_5_6_5=33635 + * @property {number} UNSIGNED_SHORT_4_4_4_4=32819 + * @property {number} UNSIGNED_SHORT_5_5_5_1=32820 + * @property {number} FLOAT=5126 + * @property {number} HALF_FLOAT=36193 + */ export const TYPES = { UNSIGNED_BYTE: 5121, UNSIGNED_SHORT: 5123, diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 3d4390c..0951de6 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -1,18 +1,15 @@ +import * as resources from './textures/resources'; +export { resources }; + export { default as Renderer } from './renderers/Renderer'; export { default as AbstractRenderer } from './renderers/AbstractRenderer'; export { default as FrameBuffer } from './textures/FrameBuffer'; export { default as CubeTexture } from './textures/CubeTexture'; export { default as BaseTexture } from './textures/BaseTexture'; -export { default as ImageResource } from './textures/resources/ImageResource'; -export { default as CanvasResource } from './textures/resources/CanvasResource'; -export { default as SVGResource } from './textures/resources/SVGResource'; -export { default as VideoResource } from './textures/resources/VideoResource'; -export { default as ArrayTexture } from './textures/ArrayTexture'; export { default as Texture } from './textures/Texture'; export { default as TextureMatrix } from './textures/TextureMatrix'; export { default as RenderTexture } from './textures/RenderTexture'; export { default as BaseRenderTexture } from './textures/BaseRenderTexture'; -export { default as VideoBaseTexture } from './textures/VideoBaseTexture'; export { default as TextureUvs } from './textures/TextureUvs'; export { default as WebGLSystem } from './renderers/systems/WebGLSystem'; export { default as State } from './renderers/State'; @@ -28,3 +25,4 @@ export { default as Attribute } from './geometry/Attribute'; export { default as Buffer } from './geometry/Buffer'; export { default as Geometry } from './geometry/Geometry'; + diff --git a/packages/core/src/renderers/Renderer.js b/packages/core/src/renderers/Renderer.js index c437741..6967107 100644 --- a/packages/core/src/renderers/Renderer.js +++ b/packages/core/src/renderers/Renderer.js @@ -243,6 +243,11 @@ // apply transform.. this.batch.currentRenderer.flush(); + if (renderTexture) + { + renderTexture.baseTexture.update(); + } + this.runners.postrender.run(); this.emit('postrender'); diff --git a/packages/core/src/renderers/TextureManager.js b/packages/core/src/renderers/TextureManager.js deleted file mode 100644 index d6d1cd6..0000000 --- a/packages/core/src/renderers/TextureManager.js +++ /dev/null @@ -1,268 +0,0 @@ -import { GLTexture } from 'pixi-gl-core'; -import { WRAP_MODES, SCALE_MODES } from '@pixi/constants'; -import RenderTarget from './utils/RenderTarget'; -import { removeItems } from '@pixi/utils'; - -/** - * Helper class to create a webGL Texture - * - * @class - * @memberof PIXI - */ -export default class TextureManager -{ - /** - * @param {PIXI.Renderer} renderer - A reference to the current renderer - */ - constructor(renderer) - { - /** - * A reference to the current renderer - * - * @member {PIXI.Renderer} - */ - this.renderer = renderer; - - /** - * The current WebGL rendering context - * - * @member {WebGLRenderingContext} - */ - this.gl = renderer.gl; - - /** - * Track textures in the renderer so we can no longer listen to them on destruction. - * - * @member {Array<*>} - * @private - */ - this._managedTextures = []; - } - - /** - * Binds a texture. - * - */ - bindTexture() - { - // empty - } - - /** - * Gets a texture. - * - */ - getTexture() - { - // empty - } - - /** - * Updates and/or Creates a WebGL texture for the renderer's context. - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to update - * @param {number} location - the location the texture will be bound to. - * @return {GLTexture} The gl texture. - */ - updateTexture(texture, location) - { - // assume it good! - // texture = texture.baseTexture || texture; - - const gl = this.gl; - - const isRenderTexture = !!texture._glRenderTargets; - - if (!texture.hasLoaded) - { - return null; - } - - const boundTextures = this.renderer.boundTextures; - - // if the location is undefined then this may have been called by n event. - // this being the case the texture may already be bound to a slot. As a texture can only be bound once - // we need to find its current location if it exists. - if (location === undefined) - { - location = 0; - - // TODO maybe we can use texture bound ids later on... - // check if texture is already bound.. - for (let i = 0; i < boundTextures.length; ++i) - { - if (boundTextures[i] === texture) - { - location = i; - break; - } - } - } - - boundTextures[location] = texture; - - gl.activeTexture(gl.TEXTURE0 + location); - - let glTexture = texture._glTextures[this.renderer.CONTEXT_UID]; - - if (!glTexture) - { - if (isRenderTexture) - { - const renderTarget = new RenderTarget( - this.gl, - texture.width, - texture.height, - texture.scaleMode, - texture.resolution - ); - - renderTarget.resize(texture.width, texture.height); - texture._glRenderTargets[this.renderer.CONTEXT_UID] = renderTarget; - glTexture = renderTarget.texture; - } - else - { - glTexture = new GLTexture(this.gl, null, null, null, null); - glTexture.bind(location); - glTexture.premultiplyAlpha = true; - glTexture.upload(texture.source); - } - - texture._glTextures[this.renderer.CONTEXT_UID] = glTexture; - - texture.on('update', this.updateTexture, this); - texture.on('dispose', this.destroyTexture, this); - - this._managedTextures.push(texture); - - if (texture.isPowerOfTwo) - { - if (texture.mipmap) - { - glTexture.enableMipmap(); - } - - if (texture.wrapMode === WRAP_MODES.CLAMP) - { - glTexture.enableWrapClamp(); - } - else if (texture.wrapMode === WRAP_MODES.REPEAT) - { - glTexture.enableWrapRepeat(); - } - else - { - glTexture.enableWrapMirrorRepeat(); - } - } - else - { - glTexture.enableWrapClamp(); - } - - if (texture.scaleMode === SCALE_MODES.NEAREST) - { - glTexture.enableNearestScaling(); - } - else - { - glTexture.enableLinearScaling(); - } - } - // the texture already exists so we only need to update it.. - else if (isRenderTexture) - { - texture._glRenderTargets[this.renderer.CONTEXT_UID].resize(texture.width, texture.height); - } - else - { - glTexture.upload(texture.source); - } - - return glTexture; - } - - /** - * Deletes the texture from WebGL - * - * @param {PIXI.BaseTexture|PIXI.Texture} texture - the texture to destroy - * @param {boolean} [skipRemove=false] - Whether to skip removing the texture from the TextureManager. - */ - destroyTexture(texture, skipRemove) - { - texture = texture.baseTexture || texture; - - if (!texture.hasLoaded) - { - return; - } - - const uid = this.renderer.CONTEXT_UID; - const glTextures = texture._glTextures; - const glRenderTargets = texture._glRenderTargets; - - if (glTextures[uid]) - { - this.renderer.unbindTexture(texture); - - glTextures[uid].destroy(); - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - - delete glTextures[uid]; - - if (!skipRemove) - { - const i = this._managedTextures.indexOf(texture); - - if (i !== -1) - { - removeItems(this._managedTextures, i, 1); - } - } - } - - if (glRenderTargets && glRenderTargets[uid]) - { - glRenderTargets[uid].destroy(); - delete glRenderTargets[uid]; - } - } - - /** - * Deletes all the textures from WebGL - */ - removeAll() - { - // empty all the old gl textures as they are useless now - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - if (texture._glTextures[this.renderer.CONTEXT_UID]) - { - delete texture._glTextures[this.renderer.CONTEXT_UID]; - } - } - } - - /** - * Destroys this manager and removes all its textures - */ - destroy() - { - // destroy managed textures - for (let i = 0; i < this._managedTextures.length; ++i) - { - const texture = this._managedTextures[i]; - - this.destroyTexture(texture, true); - - texture.off('update', this.updateTexture, this); - texture.off('dispose', this.destroyTexture, this); - } - - this._managedTextures = null; - } -} diff --git a/packages/core/src/renderers/systems/textures/GLTexture.js b/packages/core/src/renderers/systems/textures/GLTexture.js index 78d00ec..ceeb379 100644 --- a/packages/core/src/renderers/systems/textures/GLTexture.js +++ b/packages/core/src/renderers/systems/textures/GLTexture.js @@ -30,7 +30,7 @@ this.texture = gl.createTexture(); /** - * If mipmapping was used for this texture, enable and disable with enableMipmap() + * GL texture is ready for mipmaps * * @member {Boolean} */ @@ -70,6 +70,18 @@ * @member {Number} */ this.type = type || gl.UNSIGNED_BYTE; + + /** + * Texture contents dirty flag + * @member {number} + */ + this.dirtyId = -1; + + /** + * Texture style dirty flag + * @type {number} + */ + this.dirtyStyleId = -1; } /** @@ -181,104 +193,6 @@ } /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - minFilter(linear) - { - const gl = this.gl; - - this.bind(); - - if (this.mipmap) - { - /* eslint-disable max-len */ - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); - /* eslint-disable max-len */ - } - else - { - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - } - - /** - * @param linear {Boolean} if we want to use linear filtering or nearest neighbour interpolation - */ - magFilter(linear) - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST); - } - - /** - * Enables mipmapping - */ - enableMipmap() - { - const gl = this.gl; - - this.bind(); - - this.mipmap = true; - - gl.generateMipmap(gl.TEXTURE_2D); - } - - /** - * Enables linear filtering - */ - enableLinearScaling() - { - this.minFilter(true); - this.magFilter(true); - } - - /** - * Enables nearest neighbour interpolation - */ - enableNearestScaling() - { - this.minFilter(false); - this.magFilter(false); - } - - /** - * Enables clamping on the texture so WebGL will not repeat it - */ - enableWrapClamp() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - } - - enableWrapRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); - } - - enableWrapMirrorRepeat() - { - const gl = this.gl; - - this.bind(); - - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT); - } - - /** * Destroys this texture */ destroy() diff --git a/packages/core/src/renderers/systems/textures/TextureSystem.js b/packages/core/src/renderers/systems/textures/TextureSystem.js index a970311..b000db2 100644 --- a/packages/core/src/renderers/systems/textures/TextureSystem.js +++ b/packages/core/src/renderers/systems/textures/TextureSystem.js @@ -101,7 +101,6 @@ if (glTexture.dirtyId !== texture.dirtyId) { - glTexture.dirtyId = texture.dirtyId; this.updateTexture(texture); } @@ -154,117 +153,25 @@ updateTexture(texture) { const glTexture = texture._glTextures[this.CONTEXT_UID]; - const gl = this.gl; - let i; - let texturePart; - - // TODO there are only 3 textures as far as im aware? - // Cube / 2D and later 3d. (the latter is WebGL2, we will get to that soon!) - if (texture.target === gl.TEXTURE_CUBE_MAP) + if (texture.resource && texture.resource.upload(this.renderer, texture, glTexture)) { - // console.log( gl.UNSIGNED_BYTE) - for (i = 0; i < texture.sides.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.sides[i]; - - if (texturePart.resource) - { - if (texturePart.resource.uploadable) - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.format, - texture.type, - texturePart.resource.source); - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - texturePart.resource.source); - } - } - else - { - gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X + texturePart.side, - 0, - texture.format, - texture.width, - texture.height, - 0, - texture.format, - texture.type, - null); - } - } + // texture is uploaded, dont do anything! } - else if (texture.target === gl.TEXTURE_2D_ARRAY) + else if (glTexture.width !== texture.realWidth + || glTexture.height !== texture.realHeight + || glTexture.dirtyId < 0) { - gl.texImage3D(gl.TEXTURE_2D_ARRAY, - 0, - texture.format, - texture.width, - texture.height, - 6, - 0, - texture.format, - texture.type, - null); - - for (i = 0; i < texture.array.length; i++) - { - // TODO - we should only upload what changed.. - // but im sure this is not going to be a problem just yet! - texturePart = texture.array[i]; - - if (texturePart.resource) - { - if (texturePart.resource.loaded) - { - gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, - 0, - 0, // xoffset - 0, // yoffset - i, // zoffset - texturePart.resource.width, - texturePart.resource.height, - 1, - texture.format, - texture.type, - texturePart.resource.source); - } - } - } - } - else - if (texture.resource) - { - if (texture.resource.uploadable) - { - glTexture.upload(texture.resource.source); - } - else - { - glTexture.uploadData(texture.resource.source, texture.width, texture.height); - } - } - else - { - glTexture.uploadData(null, texture.width, texture.height); + // default, renderTexture-like logic + glTexture.uploadData(null, texture.realWidth, texture.realHeight); } // lets only update what changes.. - this.setStyle(texture); + if (texture.dirtyStyleId !== glTexture.dirtyStyleId) + { + this.updateTextureStyle(texture); + } + glTexture.dirtyId = texture.dirtyId; } /** @@ -298,14 +205,40 @@ } } - setStyle(texture) + updateTextureStyle(texture) + { + const glTexture = texture._glTextures[this.CONTEXT_UID]; + + glTexture.mipmap = texture.mipmap && texture.isPowerOfTwo; + if (!glTexture) + { + return; + } + + if (texture.resource && texture.resource.style(this.renderer, texture, glTexture)) + { + // style is set, dont do anything! + } + else + { + this.setStyle(texture, glTexture); + } + glTexture.dirtyStyleId = texture.dirtyStyleId; + } + + setStyle(texture, glTexture) { const gl = this.gl; + if (glTexture.mipmap) + { + gl.generateMipmap(texture.target); + } + gl.texParameteri(texture.target, gl.TEXTURE_WRAP_S, texture.wrapMode); gl.texParameteri(texture.target, gl.TEXTURE_WRAP_T, texture.wrapMode); - if (texture.mipmap) + if (glTexture.mipmap) { /* eslint-disable max-len */ gl.texParameteri(texture.target, gl.TEXTURE_MIN_FILTER, texture.scaleMode ? gl.LINEAR_MIPMAP_LINEAR : gl.NEAREST_MIPMAP_NEAREST); diff --git a/packages/core/src/textures/ArrayTexture.js b/packages/core/src/textures/ArrayTexture.js deleted file mode 100644 index 3418130..0000000 --- a/packages/core/src/textures/ArrayTexture.js +++ /dev/null @@ -1,75 +0,0 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; - -export default class ArrayTexture extends Texture -{ - constructor(width, height, size, format) - { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_2D_ARRAY; - this.size = size; - this._new = true; - this.array = []; - } - - setResource(resource, index) - { - let layer = this.array[index]; - - if (!layer) - { - layer = this.array[index] = { index, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - } - - layer.resource = resource; - - resource.load.then((resource) => - { - if (layer.resource === resource) - { - this.validate(); - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.array) - { - for (let i = 0; i < this.array.length; i++) - { - const layer = this.array[i]; - - if (layer.resource && !layer.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(width, height, ...urls) - { - const arrayTexture = new ArrayTexture(width, height); - - for (let i = 0; i < 6; i++) - { - arrayTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return arrayTexture; - } -} diff --git a/packages/core/src/textures/BaseRenderTexture.js b/packages/core/src/textures/BaseRenderTexture.js index 8e235c5..6cffdd0 100644 --- a/packages/core/src/textures/BaseRenderTexture.js +++ b/packages/core/src/textures/BaseRenderTexture.js @@ -43,18 +43,36 @@ export default class BaseRenderTexture extends BaseTexture { /** - * @param {number} [width=100] - The width of the base render texture - * @param {number} [height=100] - The height of the base render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] + * @param {number} [options.width=100] - The width of the base render texture + * @param {number} [options.height=100] - The height of the base render texture + * @param {PIXI.SCALE_MODES} [options.scaleMode] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated */ - constructor(width = 100, height = 100, scaleMode, resolution) + constructor(options) { - super(null, scaleMode, resolution, width, height); + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + // Backward compatibility of signature + const width = arguments[0]; + const height = arguments[1]; + const scaleMode = arguments[2]; + const resolution = arguments[3]; - this.width = Math.ceil(width); - this.height = Math.ceil(height); - this.hasLoaded = true; + options = { width, height, scaleMode, resolution }; + /* eslint-enable prefer-rest-params */ + } + + super(null, options); + + const { width, height } = options || {}; + + // Set defaults + this.mipmap = false; + this.width = Math.ceil(width) || 100; + this.height = Math.ceil(height) || 100; + this.valid = true; /** * A map of renderer IDs to webgl renderTargets @@ -102,9 +120,8 @@ */ resize(width, height) { - this.width = Math.ceil(width); - this.height = Math.ceil(height); - super.resize(width, height); + width = Math.ceil(width); + height = Math.ceil(height); this.frameBuffer.resize(width, height); } diff --git a/packages/core/src/textures/BaseTexture.js b/packages/core/src/textures/BaseTexture.js index c53ea23..2c80fca 100644 --- a/packages/core/src/textures/BaseTexture.js +++ b/packages/core/src/textures/BaseTexture.js @@ -1,81 +1,90 @@ import { uid, BaseTextureCache, TextureCache } from '@pixi/utils'; - import { FORMATS, TARGETS, TYPES, SCALE_MODES } from '@pixi/constants'; + +import Resource from './resources/Resource'; import BufferResource from './resources/BufferResource'; -import createResource from './resources/createResource'; +import { autoDetectResource } from './resources/autoDetectResource'; import { settings } from '@pixi/settings'; import EventEmitter from 'eventemitter3'; import bitTwiddle from 'bit-twiddle'; +/** + * A texture stores the information that represents an image. All textures have a base texture. + * + * @class + * @extends PIXI.utils.EventEmitter + * @memberof PIXI + * @param {PIXI.resources.Resource|string|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} [resource=null] + * The current resource to use, for things that aren't Resource objects, will be converted + * into a Resource. + * @param {Object} [options] - Collection of options + * @param {boolean} [options.mipmap=PIXI.settings.MIPMAP_TEXTURES] - If mipmapping is enabled for texture + * @param {PIXI.WRAP_MODES} [options.wrapMode=PIXI.settings.WRAP_MODE] - Wrap mode for textures + * @param {PIXI.SCALE_MODES} [options.scaleMode=PIXI.settings.SCALE_MODE] - Default scale mode, linear, nearest + * @param {PIXI.FORMATS} [options.format=PIXI.FORMATS.RGBA] - GL format type + * @param {PIXI.TYPES} [options.type=PIXI.TYPES.UNSIGNED_BYTE] - GL data type + * @param {PIXI.TARGETS} [options.target=PIXI.TARGETS.TEXTURE_2D] - GL texture target + * @param {boolean} [options.premultiplyAlpha=true] - Pre multiply the image alpha + * @param {number} [options.width=0] - Width of the texture + * @param {number} [options.height=0] - Height of the texture + * @param {object} [options.resourceOptions] - Optional resource options, + * see {@link PIXI.resources.autoDetectResource autoDetectResource} + */ export default class BaseTexture extends EventEmitter { - constructor(resource, - scaleMode = settings.SCALE_MODE, - resolution, - width, - height, - format, - type, - mipmap = settings.MIPMAP_TEXTURES) + constructor(resource = null, options = null) { super(); - this.uid = uid(); + options = options || {}; - this.touched = 0; + const { premultiplyAlpha, mipmap, scaleMode, width, height, + wrapMode, format, type, target, resolution, resourceOptions } = options; + + // Convert the resource to a Resource object + if (resource && !(resource instanceof Resource)) + { + resource = autoDetectResource(resource, resourceOptions); + resource.internal = true; + } /** - * The width of texture + * The width of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.width = width || -1; + this.width = width || 0; + /** - * The height of texture + * The height of the base texture set when the image has loaded * - * @member {Number} + * @readonly + * @member {number} */ - this.height = height || -1; + this.height = height || 0; /** * The resolution / device pixel ratio of the texture * * @member {number} - * @default 1 + * @default PIXI.settings.RESOLUTION */ this.resolution = resolution || settings.RESOLUTION; /** - * Whether or not the texture is a power of two, try to use power of two textures as much - * as you can - * - * @private - * @member {boolean} - */ - this.isPowerOfTwo = false; - - /** * If mipmapping was used for this texture, enable and disable with enableMipmap() * - * @member {Boolean} + * @member {boolean} */ - // TODO fix mipmapping.. - mipmap = false; - this.mipmap = mipmap; + this.mipmap = mipmap !== undefined ? mipmap : settings.MIPMAP_TEXTURES; /** - * Set to true to enable pre-multiplied alpha - * - * @member {Boolean} + * How the texture wraps + * @member {number} */ - this.premultiplyAlpha = true; - - /** - * [wrapMode description] - * @type {number} - */ - this.wrapMode = settings.WRAP_MODE; + this.wrapMode = wrapMode || settings.WRAP_MODE; /** * The scale mode to apply when scaling this texture @@ -84,42 +93,136 @@ * @default PIXI.settings.SCALE_MODE * @see PIXI.SCALE_MODES */ - this.scaleMode = scaleMode;// || settings.SCALE_MODE; + this.scaleMode = scaleMode !== undefined ? scaleMode : settings.SCALE_MODE; /** - * The pixel format of the texture. defaults to gl.RGBA + * The pixel format of the texture * - * @member {Number} + * @member {PIXI.FORMATS} + * @default PIXI.FORMATS.RGBA */ this.format = format || FORMATS.RGBA; - this.type = type || TYPES.UNSIGNED_BYTE; // UNSIGNED_BYTE - this.target = TARGETS.TEXTURE_2D; // gl.TEXTURE_2D + /** + * The type of resource data + * + * @member {PIXI.TYPES} + * @default PIXI.TYPES.UNSIGNED_BYTE + */ + this.type = type || TYPES.UNSIGNED_BYTE; + /** + * The target type + * + * @member {PIXI.TARGETS} + * @default PIXI.TARGETS.TEXTURE_2D + */ + this.target = target || TARGETS.TEXTURE_2D; + + /** + * Set to true to enable pre-multiplied alpha + * + * @member {boolean} + * @default true + */ + this.premultiplyAlpha = premultiplyAlpha !== false; + + /** + * Global unique identifier for this BaseTexture + * + * @member {string} + * @private + */ + this.uid = uid(); + + /** + * TODO: fill in description + * + * @member {number} + * @private + */ + this.touched = 0; + + /** + * Whether or not the texture is a power of two, try to use power of two textures as much + * as you can + * + * @readonly + * @member {boolean} + * @default false + */ + this.isPowerOfTwo = false; + this._refreshPOT(); + + /** + * The map of render context textures where this is bound + * + * @member {Object} + * @private + */ this._glTextures = {}; - this._new = true; - + /** + * Used by TextureSystem to only update texture to the GPU when needed. + * + * @private + * @member {number} + */ this.dirtyId = 0; - this.valid = false; + /** + * Used by TextureSystem to only update texture style when needed. + * + * @private + * @member {number} + */ + this.dirtyStyleId = 0; - this.resource = null; - - if (resource) - { - // lets convert this to a resource.. - resource = createResource(resource); - this.setResource(resource); - } - + /** + * Currently default cache ID. + * + * @member {string} + */ this.cacheId = null; - this.validate(); + /** + * Generally speaking means when resource is loaded. + * @readonly + * @member {boolean} + */ + this.valid = width > 0 && height > 0; + /** + * The collection of alternative cache ids, since some BaseTextures + * can have more than one ID, short name and longer full URL + * + * @member {Array} + * @readonly + */ this.textureCacheIds = []; /** + * Flag if BaseTexture has been destroyed. + * + * @member {boolean} + * @readonly + */ + this.destroyed = false; + + /** + * The resource used by this BaseTexture, there can only + * be one resource per BaseTexture, but textures can share + * resources. + * + * @member {PIXI.resources.Resource} + * @readonly + */ + this.resource = null; + + // Set the resource + this.setResource(resource); + + /** * Fired when a not-immediately-available source finishes loading. * * @protected @@ -168,111 +271,203 @@ */ } - updateResolution() - { - const resource = this.resource; - - if (resource && resource.width !== -1 && resource.hight !== -1) - { - this.width = resource.width / this.resolution; - this.height = resource.height / this.resolution; - } - } - - setResource(resource) - { - // TODO currently a resource can only be set once.. - - if (this.resource) - { - this.resource.resourceUpdated.remove(this); - } - - this.resource = resource; - - resource.resourceUpdated.add(this); // calls resourceUpaded - - if (resource.loaded) - { - this.resourceLoaded(resource); - } - - resource.load - .then(this.resourceLoaded.bind(this)) - .catch((reason) => - { - // failed to load - maybe resource was destroyed before it loaded. - console.warn(reason); - }); - } - - resourceLoaded(resource) - { - if (this.resource === resource) - { - this.updateResolution(); - - this.validate(); - - if (this.valid) - { - this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); - - // we have not swapped half way! - this.dirtyId++; - - this.emit('loaded', this); - } - } - } - - resourceUpdated() - { - // the resource was updated.. - this.dirtyId++; - } - - update() - { - this.dirtyId++; - } - - resize(width, height) - { - this.width = width; - this.height = height; - - this.dirtyId++; - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - this.valid = valid; - } - + /** + * Pixel width of the source of this texture + * + * @readonly + * @member {number} + */ get realWidth() { return this.width * this.resolution; } + /** + * Pixel height of the source of this texture + * + * @readonly + * @member {number} + */ get realHeight() { return this.height * this.resolution; } /** - * Destroys this base texture + * Changes style options of BaseTexture * + * @param {object} options + * @param {PIXI.SCALE_MODES} [scaleMode] - pixi scalemode + * @param {boolean} [mipmap] - enable mipmaps + * @returns {BaseTexture} this + */ + setStyle(scaleMode, mipmap) + { + let dirty; + + if (scaleMode !== undefined && scaleMode !== this.scaleMode) + { + this.scaleMode = scaleMode; + dirty = true; + } + + if (mipmap !== undefined && mipmap !== this.mipmap) + { + this.mipmap = mipmap; + dirty = true; + } + + if (dirty) + { + this.dirtyStyleId++; + } + + return this; + } + + /** + * Changes w/h/resolution. Texture becomes valid if width and height are greater than zero. + * + * @param {number} width Visual width + * @param {number} height Visual height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setSize(width, height, resolution) + { + this.resolution = resolution || this.resolution; + this.width = width; + this.height = height; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Sets real size of baseTexture, preserves current resolution. + * + * @param {number} realWidth Full rendered width + * @param {number} realHeight Full rendered height + * @param {number} [resolution] Optionally set resolution + * @returns {BaseTexture} this + */ + setRealSize(realWidth, realHeight, resolution) + { + this.resolution = resolution || this.resolution; + this.width = realWidth / this.resolution; + this.height = realHeight / this.resolution; + this._refreshPOT(); + this.update(); + + return this; + } + + /** + * Refresh check for isPowerOfTwo texture based on size + * + * @private + */ + _refreshPOT() + { + this.isPowerOfTwo = bitTwiddle.isPow2(this.realWidth) && bitTwiddle.isPow2(this.realHeight); + } + + /** + * Changes resolution + * + * @param {number} [resolution] res + * @returns {BaseTexture} this + */ + setResolution(resolution) + { + const oldResolution = this.resolution; + + if (oldResolution === resolution) + { + return this; + } + + this.resolution = resolution; + + if (this.valid) + { + this.width = this.width * oldResolution / resolution; + this.height = this.height * oldResolution / resolution; + this.emit('update'); + } + + this._refreshPOT(); + + return this; + } + + /** + * Sets the resource if it wasnt set. Throws error if resource already present + * + * @param {PIXI.resources.Resource} resource - that is managing this BaseTexture + * @returns {BaseTexture} this + */ + setResource(resource) + { + if (this.resource === resource) + { + return this; + } + + if (this.resource) + { + throw new Error('Resource can be set only once'); + } + + resource.bind(this); + + this.resource = resource; + + return this; + } + + /** + * Invalidates the object. Texture becomes valid if width and height are greater than zero. + */ + update() + { + if (!this.valid) + { + if (this.width > 0 && this.height > 0) + { + this.valid = true; + this.emit('loaded', this); + this.emit('update', this); + } + } + else + { + this.dirtyId++; + this.dirtyStyleId++; + this.emit('update', this); + } + } + + /** + * Destroys this base texture. + * The method stops if resource doesn't want this texture to be destroyed. + * Removes texture from all caches. */ destroy() { + // remove and destroy the resource + if (this.resource) + { + this.resource.unbind(this); + // only destroy resourced created internally + if (this.resource.internal) + { + this.resource.destroy(); + } + this.resource = null; + } + if (this.cacheId) { delete BaseTextureCache[this.cacheId]; @@ -281,19 +476,13 @@ this.cacheId = null; } - // remove and destroy the resource - - if (this.resource) - { - this.resource.destroy(); - this.resource = null; - } - // finally let the webGL renderer know.. this.dispose(); BaseTexture.removeFromCache(this); this.textureCacheIds = null; + + this.destroyed = true; } /** @@ -315,12 +504,12 @@ * cache, it will be created and loaded. * * @static - * @param {string|HTMLImageElement|HTMLCanvasElement} source - The source to create base texture from. - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [sourceScale=(auto)] - Scale for the original image, used with Svg images. + * @param {string|HTMLImageElement|HTMLCanvasElement|SVGElement|HTMLVideoElement} source - The + * source to create base texture from. + * @param {object} [options] See {@link PIXI.BaseTexture}'s constructor for options. * @return {PIXI.BaseTexture} The new base texture. */ - static from(source, scaleMode) + static from(source, options) { let cacheId = null; @@ -342,7 +531,7 @@ if (!baseTexture) { - baseTexture = new BaseTexture(source, scaleMode); + baseTexture = new BaseTexture(source, options); baseTexture.cacheId = cacheId; BaseTexture.addToCache(baseTexture, cacheId); } @@ -350,34 +539,30 @@ return baseTexture; } - static fromFloat32Array(width, height, float32Array) + /** + * Create a new BaseTexture with a BufferResource from a Float32Array. + * RGBA values are floats from 0 to 1. + * @static + * @param {Float32Array|UintArray} buffer The optional array to use, if no data + * is provided, a new Float32Array is created. + * @param {number} width - Width of the resource + * @param {number} height - Height of the resource + * @return {PIXI.BaseTexture} The resulting new BaseTexture + */ + static fromBuffer(buffer, width, height) { - float32Array = float32Array || new Float32Array(width * height * 4); + buffer = buffer || new Float32Array(width * height * 4); - const texture = new BaseTexture(new BufferResource(float32Array), - SCALE_MODES.NEAREST, - 1, + const resource = new BufferResource(buffer, { width, height }); + const type = buffer instanceof Float32Array ? TYPES.FLOAT : TYPES.UNSIGNED_BYTE; + + return BaseTexture(resource, { + scaleMode: SCALE_MODES.NEAREST, + format: FORMATS.RGBA, + type, width, height, - FORMATS.RGBA, - TYPES.FLOAT); - - return texture; - } - - static fromUint8Array(width, height, uint8Array) - { - uint8Array = uint8Array || new Uint8Array(width * height * 4); - - const texture = new BaseTexture(new BufferResource(uint8Array), - SCALE_MODES.NEAREST, - 1, - width, - height, - FORMATS.RGBA, - TYPES.UNSIGNED_BYTE); - - return texture; + }); } /** @@ -448,8 +633,3 @@ return null; } } - -BaseTexture.fromFrame = BaseTexture.fromFrame; -BaseTexture.fromImage = BaseTexture.from; -BaseTexture.fromSVG = BaseTexture.from; -BaseTexture.fromCanvas = BaseTexture.from; diff --git a/packages/core/src/textures/CubeTexture.js b/packages/core/src/textures/CubeTexture.js index d868951..32fd006 100644 --- a/packages/core/src/textures/CubeTexture.js +++ b/packages/core/src/textures/CubeTexture.js @@ -1,87 +1,25 @@ -import Texture from './BaseTexture'; -import ImageResource from './resources/ImageResource'; -import { TARGETS } from '@pixi/constants'; +import BaseTexture from './BaseTexture'; +import CubeResource from './resources/CubeResource'; -export default class CubeTexture extends Texture +/** + * Texture that depends on six other resources. + * + * @class + * @extends PIXI.BaseTexture + * @memberof PIXI + */ +export default class CubeTexture extends BaseTexture { - constructor(width, height, format) + /** + * Generate a new CubeTexture. + * @static + * @param {string[]|PIXI.resources.Resource[]} resources - Collection of 6 URLs or resources + * @param {object} [options] - Optional options passed to the resources being loaded. + * See {@PIXI.resources.autoDetectResource autoDetectResource} for more info + * on the options available to each resource. + */ + static from(resources, options) { - super(null, 0, 1, width, height, format); - - this.target = TARGETS.TEXTURE_CUBE_MAP; // gl.TEXTURE_CUBE_MAP - - this.resources = []; - - this.positiveX = { side: 0, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeX = { side: 1, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveY = { side: 2, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeY = { side: 3, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.positiveZ = { side: 4, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - this.negativeZ = { side: 5, texture: this, resource: null, texturePart: true, dirtyId: 0 }; - - this.sides = [this.positiveX, this.negativeX, - this.positiveY, this.negativeY, - this.positiveZ, this.negativeZ]; - } - - setResource(resource, index) - { - const side = this.sides[index]; - - side.resource = resource; - - resource.load.then((resource) => - { - if (side.resource === resource) - { - this.width = resource.width; - this.height = resource.height; - // we have not swapped half way! - // side.dirtyId++; - this.validate(); - - this.dirtyId++; - } - }); - } - - validate() - { - let valid = true; - - if (this.width === -1 || this.height === -1) - { - valid = false; - } - - if (this.sides) - { - for (let i = 0; i < this.sides.length; i++) - { - const side = this.sides[i]; - - if (side.resource && !side.resource.loaded) - { - valid = false; - break; - } - } - } - - this.valid = valid; - } - - static from(...urls) - { - const cubeTexture = new CubeTexture(); - - for (let i = 0; i < 6; i++) - { - cubeTexture.setResource(ImageResource.from(urls[i % urls.length]), i); - } - - return cubeTexture; + return new CubeTexture(new CubeResource(resources, options)); } } diff --git a/packages/core/src/textures/FrameBuffer.js b/packages/core/src/textures/FrameBuffer.js index 763b857..5a42736 100644 --- a/packages/core/src/textures/FrameBuffer.js +++ b/packages/core/src/textures/FrameBuffer.js @@ -1,6 +1,11 @@ import Texture from './BaseTexture'; import { FORMATS, TYPES } from '@pixi/constants'; +/** + * Frame buffer + * @class + * @memberof PIXI + */ export default class FrameBuffer { constructor(width, height) @@ -81,12 +86,12 @@ for (let i = 0; i < this.colorTextures.length; i++) { - this.colorTextures[i].resize(width, height); + this.colorTextures[i].setSize(width, height); } if (this.depthTexture) { - this.depthTexture.resize(width, height); + this.depthTexture.setSize(width, height); } } } diff --git a/packages/core/src/textures/RenderTexture.js b/packages/core/src/textures/RenderTexture.js index 4fe047f..99f6353 100644 --- a/packages/core/src/textures/RenderTexture.js +++ b/packages/core/src/textures/RenderTexture.js @@ -63,7 +63,12 @@ /* eslint-enable prefer-rest-params, no-console */ frame = null; - baseRenderTexture = new BaseRenderTexture(width, height, scaleMode, resolution); + baseRenderTexture = new BaseRenderTexture({ + width, + height, + scaleMode, + resolution, + }); } /** @@ -71,10 +76,7 @@ * * @member {BaseTexture} */ - super( - baseRenderTexture, - frame - ); + super(baseRenderTexture, frame); this.legacyRenderer = _legacyRenderer; @@ -93,9 +95,9 @@ * * @param {number} width - The width to resize to. * @param {number} height - The height to resize to. - * @param {boolean} doNotResizeBaseTexture - Should the baseTexture.width and height values be resized as well? + * @param {boolean} [resizeBaseTexture=true] - Should the baseTexture.width and height values be resized as well? */ - resize(width, height, doNotResizeBaseTexture) + resize(width, height, resizeBaseTexture = true) { width = Math.ceil(width); height = Math.ceil(height); @@ -106,7 +108,7 @@ this._frame.width = this.orig.width = width; this._frame.height = this.orig.height = height; - if (!doNotResizeBaseTexture) + if (resizeBaseTexture) { this.baseTexture.resize(width, height); } @@ -117,14 +119,28 @@ /** * A short hand way of creating a render texture. * - * @param {number} [width=100] - The width of the render texture - * @param {number} [height=100] - The height of the render texture - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @param {number} [resolution=1] - The resolution / device pixel ratio of the texture being generated + * @param {object} [options] - Options + * @param {number} [options.width=100] - The width of the render texture + * @param {number} [options.height=100] - The height of the render texture + * @param {number} [options.scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values + * @param {number} [options.resolution=1] - The resolution / device pixel ratio of the texture being generated * @return {PIXI.RenderTexture} The new render texture */ - static create(width, height, scaleMode, resolution) + static create(options) { - return new RenderTexture(new BaseRenderTexture(width, height, scaleMode, resolution)); + // fallback, old-style: create(width, height, scaleMode, resolution) + if (typeof options === 'number') + { + /* eslint-disable prefer-rest-params */ + options = { + width: options, + height: arguments[1], + scaleMode: arguments[2], + resolution: arguments[3], + }; + /* eslint-enable prefer-rest-params */ + } + + return new RenderTexture(new BaseRenderTexture(options)); } } diff --git a/packages/core/src/textures/Texture.js b/packages/core/src/textures/Texture.js index 2f75852..b677121 100644 --- a/packages/core/src/textures/Texture.js +++ b/packages/core/src/textures/Texture.js @@ -30,7 +30,7 @@ * You can use a ticker or rAF to ensure your sprites load the finished textures after processing. See issue #3068. * * @class - * @extends EventEmitter + * @extends PIXI.utils.EventEmitter * @memberof PIXI */ export default class Texture extends EventEmitter @@ -323,14 +323,15 @@ static fromLoader(source, imageUrl, name) { // console.log('added from loader...') - const resource = new ImageResource(source);// .from(imageUrl, crossorigin);// document.createElement('img'); + const resource = new ImageResource(source); resource.url = imageUrl; // console.log('base resource ' + resource.width); - const baseTexture = new BaseTexture(resource, - settings.SCALE_MODE, - getResolutionOfUrl(imageUrl)); + const baseTexture = new BaseTexture(resource, { + scaleMode: settings.SCALE_MODE, + resultion: getResolutionOfUrl(imageUrl), + }); const texture = new Texture(baseTexture); @@ -457,7 +458,6 @@ + `${errorX} ${relationship} ${errorY}`); } - this.valid = frame && frame.width && frame.height && this.baseTexture.hasLoaded; this.valid = width && height && this.baseTexture.valid; if (!this.trim && !this.rotate) diff --git a/packages/core/src/textures/VideoBaseTexture.js b/packages/core/src/textures/VideoBaseTexture.js deleted file mode 100644 index d0b7fbf..0000000 --- a/packages/core/src/textures/VideoBaseTexture.js +++ /dev/null @@ -1,323 +0,0 @@ -import BaseTexture from './BaseTexture'; -import { uid, BaseTextureCache, determineCrossOrigin } from '@pixi/utils'; -import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; - -/** - * A texture of a [playing] Video. - * - * Video base textures mimic PixiJS BaseTexture.from.... method in their creation process. - * - * This can be used in several ways, such as: - * - * ```js - * let texture = PIXI.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4'); - * - * let texture = PIXI.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' }); - * - * let texture = PIXI.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']); - * - * let texture = PIXI.VideoBaseTexture.fromUrls([ - * { src: '/video.webm', mime: 'video/webm' }, - * { src: '/video.mp4', mime: 'video/mp4' } - * ]); - * ``` - * - * See the ["deus" demo](http://www.goodboydigital.com/pixijs/examples/deus/). - * - * @class - * @extends PIXI.BaseTexture - * @memberof PIXI - */ -export default class VideoBaseTexture extends BaseTexture -{ - /** - * @param {HTMLVideoElement} source - Video source - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - */ - constructor(source, scaleMode) - { - if (!source) - { - throw new Error('No video source element specified.'); - } - - // hook in here to check if video is already available. - // BaseTexture looks for a source.complete boolean, plus width & height. - - if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) - { - source.complete = true; - } - - super(source, scaleMode); - - this.width = source.videoWidth; - this.height = source.videoHeight; - - this._autoUpdate = true; - this._isAutoUpdating = false; - - /** - * When set to true will automatically play videos used by this texture once - * they are loaded. If false, it will not modify the playing state. - * - * @member {boolean} - * @default true - */ - this.autoPlay = true; - - this.update = this.update.bind(this); - this._onCanPlay = this._onCanPlay.bind(this); - - source.addEventListener('play', this._onPlayStart.bind(this)); - source.addEventListener('pause', this._onPlayStop.bind(this)); - this.hasLoaded = false; - this.__loaded = false; - - if (!this._isSourceReady()) - { - source.addEventListener('canplay', this._onCanPlay); - source.addEventListener('canplaythrough', this._onCanPlay); - } - else - { - this._onCanPlay(); - } - } - - /** - * Returns true if the underlying source is playing. - * - * @private - * @return {boolean} True if playing. - */ - _isSourcePlaying() - { - const source = this.source; - - return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2); - } - - /** - * Returns true if the underlying source is ready for playing. - * - * @private - * @return {boolean} True if ready. - */ - _isSourceReady() - { - return this.source.readyState === 3 || this.source.readyState === 4; - } - - /** - * Runs the update loop when the video is ready to play - * - * @private - */ - _onPlayStart() - { - // Just in case the video has not received its can play even yet.. - if (!this.hasLoaded) - { - this._onCanPlay(); - } - - if (!this._isAutoUpdating && this.autoUpdate) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - - /** - * Fired when a pause event is triggered, stops the update loop - * - * @private - */ - _onPlayStop() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - } - - /** - * Fired when the video is loaded and ready to play - * - * @private - */ - _onCanPlay() - { - this.hasLoaded = true; - - if (this.source) - { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); - - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.__loaded) - { - this.__loaded = true; - this.emit('loaded', this); - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } - } - } - - /** - * Destroys this texture - * - */ - destroy() - { - if (this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - } - - if (this.source && this.source._pixiId) - { - BaseTexture.removeFromCache(this.source._pixiId); - delete this.source._pixiId; - } - - super.destroy(); - } - - /** - * Mimic PixiJS BaseTexture.from.... method. - * - * @static - * @param {HTMLVideoElement} video - Video to create texture from - * @param {number} [scaleMode=PIXI.settings.SCALE_MODE] - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromVideo(video, scaleMode) - { - if (!video._pixiId) - { - video._pixiId = `video_${uid()}`; - } - - let baseTexture = BaseTextureCache[video._pixiId]; - - if (!baseTexture) - { - baseTexture = new VideoBaseTexture(video, scaleMode); - BaseTexture.addToCache(baseTexture, video._pixiId); - } - - return baseTexture; - } - - /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture - * - * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI. - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture - */ - static fromUrl(videoSrc, scaleMode, crossorigin) - { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - video.crossOrigin = determineCrossOrigin(url); - } - else if (crossorigin) - { - video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; - } - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(url, videoSrc.mime)); - } - - video.load(); - - return VideoBaseTexture.fromVideo(video, scaleMode); - } - - /** - * Should the base texture automatically update itself, set to true by default - * - * @member {boolean} - */ - get autoUpdate() - { - return this._autoUpdate; - } - - set autoUpdate(value) // eslint-disable-line require-jsdoc - { - if (value !== this._autoUpdate) - { - this._autoUpdate = value; - - if (!this._autoUpdate && this._isAutoUpdating) - { - Ticker.shared.remove(this.update, this); - this._isAutoUpdating = false; - } - else if (this._autoUpdate && !this._isAutoUpdating) - { - Ticker.shared.add(this.update, this, UPDATE_PRIORITY.HIGH); - this._isAutoUpdating = true; - } - } - } -} - -VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl; - -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} diff --git a/packages/core/src/textures/resources/ArrayResource.js b/packages/core/src/textures/resources/ArrayResource.js new file mode 100644 index 0000000..9c21902 --- /dev/null +++ b/packages/core/src/textures/resources/ArrayResource.js @@ -0,0 +1,242 @@ +import Resource from './Resource'; +import BaseTexture from '../BaseTexture'; +import { TARGETS } from '@pixi/constants'; +import { autoDetectResource } from './autoDetectResource'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + * @param {number|Array<*>} source - Number of items in array or the collection + * of image URLs to use. Can also be resources, image elements, canvas, etc. + * @param {object} [options] Options to apply to {@link PIXI.resources.autoDetectResource} + * @param {number} [options.width] - Width of the resource + * @param {number} [options.height] - Height of the resource + */ +export default class ArrayResource extends Resource +{ + constructor(source, options) + { + options = options || {}; + + let urls; + let length = source; + + if (Array.isArray(source)) + { + urls = source; + length = source.length; + } + + super(options.width, options.height); + + /** + * Collection of resources. + * @member {Array} + * @readonly + */ + this.items = []; + + /** + * Dirty IDs for each part + * @member {Array} + * @readonly + */ + this.itemDirtyIds = []; + + for (let i = 0; i < length; i++) + { + const partTexture = new BaseTexture(); + + this.items.push(partTexture); + this.itemDirtyIds.push(-1); + } + + /** + * Number of elements in array + * + * @member {number} + * @readonly + */ + this.length = length; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (urls) + { + for (let i = 0; i < length; i++) + { + this.addResourceAt(autoDetectResource(urls[i], options), i); + } + } + } + + /** + * Destroy this BaseImageResource + * @override + */ + dispose() + { + for (let i = 0, len = this.length; i < len; i++) + { + this.items[i].destroy(); + } + this.items = null; + this.itemDirtyIds = null; + this._load = null; + } + + /** + * Set a resource by ID + * + * @param {PIXI.resources.Resource} resource + * @param {number} index - Zero-based index of resource to set + * @return {PIXI.resources.ArrayResource} Instance for chaining + */ + addResourceAt(resource, index) + { + const baseTexture = this.items[index]; + + if (!baseTexture) + { + throw new Error(`Index ${index} is out of bounds`); + } + + // Inherit the first resource dimensions + if (resource.valid && !this.valid) + { + this.resize(resource.width, resource.height); + } + + this.items[index].setResource(resource); + + return this; + } + + /** + * Set the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_2D_ARRAY; + + for (let i = 0; i < this.length; i++) + { + this.items[i].on('update', baseTexture.update, baseTexture); + } + } + + /** + * Unset the parent base texture + * @member {PIXI.BaseTexture} + * @override + */ + unbind(baseTexture) + { + super.unbind(baseTexture); + + for (let i = 0; i < this.length; i++) + { + this.items[i].off('update', baseTexture.update, baseTexture); + } + } + + /** + * Load all the resources simultaneously + * @override + * @return {Promise} When load is resolved + */ + load() + { + if (this._load) + { + return this._load; + } + + const resources = this.items.map((item) => item.resource); + + // TODO: also implement load part-by-part strategy + const promises = resources.map((item) => item.load()); + + this._load = Promise.all(promises) + .then(() => + { + const { width, height } = resources[0]; + + this.resize(width, height); + + return Promise.resolve(this); + } + ); + + return this._load; + } + + /** + * Upload the resources to the GPU. + * @param {PIXI.Renderer} renderer + * @param {PIXI.BaseTexture} texture + * @param {PIXI.glCore.Texture} glTexture + */ + upload(renderer, texture, glTexture) + { + const { length, itemDirtyIds, items } = this; + const { gl } = renderer; + + if (glTexture.dirtyId < 0) + { + gl.texImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + texture.format, + this._width, + this._height, + length, + 0, + texture.format, + texture.type, + null + ); + } + + for (let i = 0; i < length; i++) + { + const item = items[i]; + + if (itemDirtyIds[i] < item.dirtyId) + { + itemDirtyIds[i] = item.dirtyId; + if (item.valid) + { + gl.texSubImage3D( + gl.TEXTURE_2D_ARRAY, + 0, + 0, // xoffset + 0, // yoffset + i, // zoffset + item.resource.width, + item.resource.height, + 1, + texture.format, + texture.type, + item.resource.source + ); + } + } + } + + return true; + } +} diff --git a/packages/core/src/textures/resources/BaseImageResource.js b/packages/core/src/textures/resources/BaseImageResource.js new file mode 100644 index 0000000..6e490fa --- /dev/null +++ b/packages/core/src/textures/resources/BaseImageResource.js @@ -0,0 +1,72 @@ +import Resource from './Resource'; +import { determineCrossOrigin } from '@pixi/utils'; + +/** + * Base for all the image/canvas resources + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BaseImageResource extends Resource +{ + /** + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} source + */ + constructor(source) + { + super(source.width, source.height); + + /** + * The source element + * @member {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement|SVGElement} + * @readonly + */ + this.source = source; + } + + /** + * Set cross origin based detecting the url and the crossorigin + * @protected + * @param {HTMLElement} element - Element to apply crossOrigin + * @param {string} url - URL to check + * @param {boolean|string} [crossorigin=true] - Cross origin value to use + */ + static crossOrigin(element, url, crossorigin) + { + if (crossorigin === undefined && url.indexOf('data:') !== 0) + { + element.crossOrigin = determineCrossOrigin(url); + } + else if (crossorigin !== false) + { + element.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous'; + } + } + + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + * @param {PIXI.glCore.GLTexture} glTexture Reference + */ + upload(renderer, baseTexture, glTexture) + { + // TODO: move glTexture.upload somewhere else + glTexture.format = baseTexture.format; + glTexture.type = baseTexture.type; + glTexture.upload(this.source); + + return true; + } + + /** + * Destroy this BaseImageResource + * @override + * @param {PIXI.BaseTexture} [fromTexture] Optional base texture + * @return {boolean} Destroy was successful + */ + dispose() + { + this.source = null; + } +} diff --git a/packages/core/src/textures/resources/BufferResource.js b/packages/core/src/textures/resources/BufferResource.js index 1aa1586..7f1c12c 100644 --- a/packages/core/src/textures/resources/BufferResource.js +++ b/packages/core/src/textures/resources/BufferResource.js @@ -1,21 +1,81 @@ -import TextureResource from './TextureResource'; +import Resource from './Resource'; -export default class BufferResource extends TextureResource +/** + * Buffer resource with data of typed array. + * @class + * @extends PIXI.resources.Resource + * @memberof PIXI.resources + */ +export default class BufferResource extends Resource { - constructor(source) + /** + * @param {Float32Array|Uint8Array|Uint32Array} source - Source buffer + * @param {object} options - Options + * @param {number} options.width - Width of the texture + * @param {number} options.height - Height of the texture + */ + constructor(source, options) { - super(source); + const { width, height } = options || {}; - this.uploadable = false; - - this.load = new Promise((resolve) => + if (!width || !height) { - resolve(this); - }); + throw new Error('BufferResource width or height invalid'); + } + + super(width, height); + + /** + * Source array + * Cannot be ClampedUint8Array because it cant be uploaded to WebGL + * + * @member {Float32Array|Uint8Array|Uint32Array} + */ + this.data = source; } - static from(array) + /** + * Upload the texture to the GPU. + * @param {PIXI.Renderer} renderer Upload to the renderer + * @param {PIXI.BaseTexture} baseTexture Reference to parent texture + */ + upload(renderer, baseTexture) { - return new BufferResource(array); + renderer.gl.texImage2D( + baseTexture.target, + 0, + baseTexture.format, + baseTexture.width, + baseTexture.height, + 0, + baseTexture.format, + baseTexture.type, + this.data + ); + + return true; + } + + /** + * Destroy and don't use after this + * @override + */ + dispose() + { + this.data = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) + { + return source instanceof Float32Array + || source instanceof Uint8Array + || source instanceof Uint32Array; } } diff --git a/packages/core/src/textures/resources/CanvasResource.js b/packages/core/src/textures/resources/CanvasResource.js index 14914c5..7ba0eaf 100644 --- a/packages/core/src/textures/resources/CanvasResource.js +++ b/packages/core/src/textures/resources/CanvasResource.js @@ -1,32 +1,23 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for HTMLCanvasElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources * @param {HTMLCanvasElement} source - Canvas element to use */ -export default class CanvasResource extends TextureResource +export default class CanvasResource extends BaseImageResource { - constructor(source) + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @return {boolean} `true` if + */ + static test(source) { - super(source); - - this.loaded = true; // TODO rename to ready? - this.width = source.width; - this.height = source.height; - - this.uploadable = true; - - this.load = new Promise((resolve) => - { - resolve(this); - }); - } - - static from(canvas) - { - return new CanvasResource(canvas); + return (source instanceof HTMLCanvasElement); } } diff --git a/packages/core/src/textures/resources/CubeResource.js b/packages/core/src/textures/resources/CubeResource.js new file mode 100644 index 0000000..87c9319 --- /dev/null +++ b/packages/core/src/textures/resources/CubeResource.js @@ -0,0 +1,76 @@ +import ArrayResource from './ArrayResource'; +import { TARGETS } from '@pixi/constants'; + +/** + * Resource for a CubeTexture which contains six resources. + * + * @class + * @extends PIXI.resources.ArrayResource + * @memberof PIXI.resources + * @param {Array} [source] Collection of URLs or resoures + * to use as the sides of the cube. + * @param {object} [options] - ImageResource options + * @param {number} [options.width] - Width of resource + * @param {number} [options.height] - Height of resource + */ +export default class CubeResource extends ArrayResource +{ + constructor(source, options) + { + super(source, options); + + if (this.length !== CubeResource.SIDES) + { + throw new Error(`Invalid length. Got ${this.length}, expected 6`); + } + } + + /** + * Add binding + * @override + * @param {PIXI.BaseTexture} baseTexture - parent base texture + */ + bind(baseTexture) + { + super.bind(baseTexture); + + baseTexture.target = TARGETS.TEXTURE_CUBE_MAP; + } + + /** + * Uploade the resource + */ + upload(renderer, baseTexture, glTexture) + { + const dirty = this.itemDirtyIds; + + for (let i = 0; i < CubeResource.SIDES; i++) + { + const side = this.items[i]; + + if (dirty[i] < side.dirtyId) + { + dirty[i] = side.dirtyId; + if (side.valid) + { + side.resource.upload(renderer, side, glTexture); + } + else + { + // TODO: upload zero buffer + } + } + } + + return true; + } +} + +/** + * Number of texture sides to store for CubeResources + * @name PIXI.resources.CubeResource.SIDES + * @static + * @member {number} + * @default 6 + */ +CubeResource.SIDES = 6; diff --git a/packages/core/src/textures/resources/ImageResource.js b/packages/core/src/textures/resources/ImageResource.js index bfae6cf..a4b3ca6 100644 --- a/packages/core/src/textures/resources/ImageResource.js +++ b/packages/core/src/textures/resources/ImageResource.js @@ -1,41 +1,123 @@ -import { determineCrossOrigin } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; +import { settings } from '@pixi/settings'; /** * Resource type for HTMLImageElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLImageElement} source - Image element to use + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources */ -export default class ImageResource extends TextureResource +export default class ImageResource extends BaseImageResource { - constructor(source) + /** + * @param {HTMLImageElement|string} source - image source or URL + * @param {boolean} [options.autoLoad=true] start loading process + * @param {boolean} [options.createBitmap=true] whether its required to create + * a bitmap before upload defaults true + * @param {boolean} [options.crossorigin=true] - Load image using cross origin + */ + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLImageElement)) + { + const imageElement = new Image(); + + BaseImageResource.crossOrigin(imageElement, source, options.crossorigin); + + imageElement.src = source; + source = imageElement; + } + super(source); + /** + * URL of the image source + * @member {string} + */ this.url = source.src; - this.load = new Promise((resolve) => - { - const source = this.source; + /** + * When process is completed + * @member {Promise} + * @private + */ + this._process = null; - source.onload = () => + /** + * If the image should be disposed after upload + * @member {boolean} + * @default false + */ + this.preserveBitmap = false; + + /** + * If capable, convert the image using createImageBitmap API + * @member {boolean} + * @default PIXI.settings.CREATE_IMAGE_BITMAP + */ + this.createBitmap = options.createBitmap !== false && settings.CREATE_IMAGE_BITMAP && !!window.createImageBitmap; + + /** + * The ImageBitmap element created for HTMLImageElement + * @member {ImageBitmap} + * @default null + */ + this.bitmap = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) + { + this.load(); + } + } + + /** + * returns a promise when image will be loaded and processed + * + * @param {boolean} [createBitmap=true] whether process image into bitmap + * @returns {Promise} + */ + load(createBitmap) + { + if (createBitmap !== undefined) + { + this.createBitmap = createBitmap; + } + + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + this.url = this.source.src; + const { source } = this; + + const completed = () => { - this.loaded = true; + if (this.destroyed) + { + return; + } source.onload = null; source.onerror = null; - this.width = source.width; - this.height = source.height; - if (window.createImageBitmap) + this.resize(source.width, source.height); + this._load = null; + + if (this.createBitmap) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + resolve(this.process()); } else { @@ -45,53 +127,103 @@ if (source.complete && source.src) { - this.loaded = true; - source.onload = null; - source.onerror = null; - this.width = source.width; - this.height = source.height; + completed(); + } + else + { + source.onload = completed; + } + }); - if (window.createImageBitmap) + return this._load; + } + + /** + * Called when we need to convert image into BitmapImage. + * Can be called multiple times, real promise is cached inside. + * + * @returns {Promise} cached promise to fill that bitmap + */ + process() + { + if (this._process !== null) + { + return this._process; + } + if (this.bitmap !== null || !window.createImageBitmap) + { + return Promise.resolve(this); + } + + this._process = window.createImageBitmap(this.source) + .then((bitmap) => + { + if (this.destroyed) { - window.createImageBitmap(source).then((imageBitmap) => - { - this.source = imageBitmap; - - resolve(this); - }); + return Promise.reject(); } - else + this.bitmap = bitmap; + this.update(); + this._process = null; + + return Promise.resolve(this); + }); + + return this._process; + } + + /** + * Upload the image resource to GPU. + * + * @param {PIXI.Renderer} renderer - Renderer to upload to + * @param {PIXI.BaseTexture} baseTexture - BaseTexture for this resource + * @param {PIXI.glCore.Texture} glTexture - GLTexture to use + */ + upload(renderer, baseTexture, glTexture) + { + if (this.createBitmap) + { + if (!this.bitmap) + { + // yeah, ignore the output + this.process(); + if (!this.bitmap) { - resolve(this); + return false; } } - - // source.onerror = () => { - // reject('unable to load "' + source.src + '" resource cannot be found') - // } - }); - } - - destroy() - { - this.source.src = ''; - } - - static from(url, crossorigin) - { - const image = new Image(); - - if (crossorigin === undefined && url.indexOf('data:') !== 0) - { - image.crossOrigin = determineCrossOrigin(url); + glTexture.upload(this.bitmap); + if (!this.preserveBitmap) + { + if (this.bitmap.close) + { + this.bitmap.close(); + } + this.bitmap = null; + } } - else if (crossorigin) + else { - image.crossOrigin = 'anonymous'; + super.upload(renderer, baseTexture, glTexture); } - image.src = url; + return true; + } - return new ImageResource(image); + /** + * Destroys this texture + * @override + */ + dispose() + { + super.dispose(); + + if (this.bitmap) + { + this.bitmap.close(); + this.bitmap = null; + } + this._process = null; + this._load = null; } } diff --git a/packages/core/src/textures/resources/Resource.js b/packages/core/src/textures/resources/Resource.js new file mode 100644 index 0000000..3c239ef --- /dev/null +++ b/packages/core/src/textures/resources/Resource.js @@ -0,0 +1,213 @@ +import Runner from 'mini-runner'; + +/** + * Base Texture resource class, manages validation and upload depends on its type. + * upload is required. + * @class + * @memberof PIXI.resources + */ +export default class Resource +{ + /** + * @param {number} [width=0] Width of the resource + * @param {number} [height=0] Height of the resource + */ + constructor(width = 0, height = 0) + { + /** + * Internal width of the resource + * @member {number} + * @protected + */ + this._width = width; + + /** + * Internal height of the resource + * @member {number} + * @protected + */ + this._height = height; + + /** + * If resource has been destroyed + * @member {boolean} + * @readonly + * @default false + */ + this.destroyed = false; + + /** + * `true` if resource is created by BaseTexture + * useful for doing cleanup with BaseTexture destroy + * and not cleaning up resources that were created + * externally. + * @member {boolean} + * @private + */ + this.internal = false; + + /** + * Mini-runner for handling resize events + * + * @member {Runner} + */ + this.onResize = new Runner('setRealSize', 2); + + /** + * Mini-runner for handling update events + * + * @member {Runner} + */ + this.onUpdate = new Runner('update'); + } + + /** + * Bind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + bind(baseTexture) + { + this.onResize.add(baseTexture); + this.onUpdate.add(baseTexture); + + // Call a resize immediate if we already + // have the width and height of the resource + if (this._width || this._height) + { + this.onResize.run(this._width, this._height); + } + } + + /** + * Unbind to a parent BaseTexture + * + * @param {PIXI.BaseTexture} baseTexture - Parent texture + */ + unbind(baseTexture) + { + this.onResize.remove(baseTexture); + this.onUpdate.remove(baseTexture); + } + + /** + * Trigger a resize event + */ + resize(width, height) + { + if (width !== this._width || height !== this._height) + { + this._width = width; + this._height = height; + this.onResize.run(width, height); + } + } + + /** + * Has been validated + * @readonly + * @member {boolean} + */ + get valid() + { + return !!this._width && !!this._height; + } + + /** + * Has been updated trigger event + */ + update() + { + if (!this.destroyed) + { + this.onUpdate.run(); + } + } + + /** + * This can be overriden to start preloading a resource + * or do any other prepare step. + * @protected + * @return {Promise} Handle the validate event + */ + load() + { + return Promise.resolve(); + } + + /** + * The width of the resource. + * + * @member {number} + * @readonly + */ + get width() + { + return this._width; + } + + /** + * The height of the resource. + * + * @member {number} + * @readonly + */ + get height() + { + return this._height; + } + + /** + * Uploads the texture or returns false if it cant for some reason. Override this. + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} true is success + */ + upload(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Set the style, optional to override + * + * @param {PIXI.Renderer} renderer - yeah, renderer! + * @param {PIXI.BaseTexture} baseTexture - the texture + * @param {PIXI.glCore.Texture} glTexture - texture instance for this webgl context + * @returns {boolean} `true` is success + */ + style(renderer, baseTexture, glTexture) // eslint-disable-line no-unused-vars + { + return false; + } + + /** + * Clean up anything, this happens when destroying is ready. + * + * @protected + */ + dispose() + { + // override + } + + /** + * Call when destroying resource, unbind any BaseTexture object + * before calling this method, as reference counts are maintained + * internally. + */ + destroy() + { + if (!this.destroyed) + { + this.onResize.removeAll(); + this.onResize = null; + this.onUpdate.removeAll(); + this.onUpdate = null; + this.destroyed = true; + this.dispose(); + } + } +} diff --git a/packages/core/src/textures/resources/SVGResource.js b/packages/core/src/textures/resources/SVGResource.js index 307b908..b6a98e9 100644 --- a/packages/core/src/textures/resources/SVGResource.js +++ b/packages/core/src/textures/resources/SVGResource.js @@ -1,60 +1,131 @@ import { decomposeDataUri, uid } from '@pixi/utils'; -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; /** * Resource type for SVG elements and graphics. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {SVGResource} svgSource - Source SVG element. - * @param {number} [scale=1] Scale to apply to SVG. + * @extends PIXI.resources.TextureResource + * @memberof PIXI.resources + * @param {string} source - Base64 encoded SVG element or URL for SVG file. + * @param {object} [options] - Options to use + * @param {number} [options.scale=1] Scale to apply to SVG. + * @param {boolean} [options.autoLoad=true] Start loading right away. */ -export default class SVGResource extends TextureResource +export default class SVGResource extends BaseImageResource { - constructor(svgSource, scale = 1) + constructor(source, options) { - super(); + options = options || {}; - this.svgSource = svgSource; - this.scale = scale; - this.uploadable = true; + super(document.createElement('canvas')); - this.resolve = null; + /** + * Base64 encoded SVG element or URL for SVG file + * @readonly + * @member {string} + */ + this.svg = source; - this.load = new Promise((resolve) => + /** + * The source scale to apply to render + * @readonly + * @member {number} + */ + this.scale = options.scale || 1; + + /** + * Call when completedly loaded + * @private + * @member {function} + */ + this._resolve = null; + + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + if (options.autoLoad !== false) { - this.resolve = resolve; - this._loadSvgSourceUsingXhr(); + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + this._load = new Promise((resolve) => + { + // Save this until after load is finished + this._resolve = () => + { + this.resize(this.source.width, this.source.height); + resolve(this); + }; + + // Convert SVG inline string to data-uri + if (/^\ this.emit('error', this); + // svgXhr.onerror = () => this.emit('error', this); - svgXhr.open('GET', this.svgSource, true); + svgXhr.open('GET', this.svg, true); svgXhr.send(); } /** * Loads texture using an SVG string. The original SVG Image is stored as `origSource` and the * created canvas is the new `source`. The SVG is scaled using `sourceScale`. Called by - * `_loadSvgSourceUsingXhr` or `_loadSvgSourceUsingDataUri`. + * `_loadXhr` or `_loadDataUri`. * * @private * @param {string} svgString SVG source as string * * @fires loaded */ - _loadSvgSourceUsingString(svgString) + _loadString(svgString) { const svgSize = SVGResource.getSize(svgString); // TODO do we need to wait for this to load? // seems instant! // - const tempImage = new Image(); + const tempImage = new Image(); tempImage.src = `data:image/svg+xml,${svgString}`; @@ -111,14 +182,14 @@ } // Scale realWidth and realHeight - this.width = Math.round(svgWidth * this.scale); - this.height = Math.round(svgHeight * this.scale); + this._width = Math.round(svgWidth * this.scale); + this._height = Math.round(svgHeight * this.scale); // Create a canvas element - const canvas = document.createElement('canvas'); + const canvas = this.source; - canvas.width = this.width; - canvas.height = this.height; + canvas.width = this._width; + canvas.height = this._height; canvas._pixiId = `canvas_${uid()}`; // Draw the Svg to the canvas @@ -126,15 +197,14 @@ .getContext('2d') .drawImage(tempImage, 0, 0, svgWidth, svgHeight, 0, 0, this.width, this.height); - this.source = canvas; - - this.resolve(this); + this._resolve(); + this._resolve = null; } /** * Typedef for Size object. * - * @typedef {object} PIXI.SVGResource~Size + * @typedef {object} PIXI.resources.SVGResource~Size * @property {number} width - Width component * @property {number} height - Height component */ @@ -144,7 +214,7 @@ * * @method * @param {string} svgString - a serialized svg element - * @return {PIXI.SVGResource~Size} image extension + * @return {PIXI.resources.SVGResource~Size} image extension */ static getSize(svgString) { @@ -160,28 +230,39 @@ return size; } - static from(url) + /** + * Destroys this texture + * @override + */ + dispose() { - return new SVGResource(url); + super.dispose(); + this._resolve = null; + } + + /** + * Used to auto-detect the type of resource. + * + * @static + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + */ + static test(source, extension) + { + // url file extension is SVG + return extension === 'svg' + // source is SVG data-uri + || (typeof source === 'string' && source.indexOf('data:image/svg+xml') === 0); } } /** - * List of common SVG file extensions supported by SVGResource. - * @constant - * @member {Array} - * @static - * @readonly - */ -SVGResource.TYPES = ['svg']; - -/** * RegExp for SVG size. * * @static * @constant * @name SVG_SIZE - * @memberof PIXI.SVGResource + * @memberof PIXI.resources.SVGResource * @type {RegExp|string} * @example <svg width="100" height="100"></svg> */ diff --git a/packages/core/src/textures/resources/TextureResource.js b/packages/core/src/textures/resources/TextureResource.js deleted file mode 100644 index 5c5f2be..0000000 --- a/packages/core/src/textures/resources/TextureResource.js +++ /dev/null @@ -1,32 +0,0 @@ -import Runner from 'mini-runner'; - -/** - * Base Texture resource class. - * @class - * @memberof PIXI - * @param {any} source - Source element to use. - */ -export default class TextureResource -{ - constructor(source) - { - this.source = source; - - this.loaded = false; // TODO rename to ready? - - this.width = -1; - this.height = -1; - - this.uploadable = true; - - this.resourceUpdated = new Runner('resourceUpdated'); - - // create a prommise.. - this.load = null; - } - - destroy() - { - // somthing - } -} diff --git a/packages/core/src/textures/resources/VideoResource.js b/packages/core/src/textures/resources/VideoResource.js index e79fbb3..246c1c8 100644 --- a/packages/core/src/textures/resources/VideoResource.js +++ b/packages/core/src/textures/resources/VideoResource.js @@ -1,17 +1,56 @@ -import TextureResource from './TextureResource'; +import BaseImageResource from './BaseImageResource'; import { Ticker } from '@pixi/ticker'; /** * Resource type for HTMLVideoElement. * @class - * @extends PIXI.TextureResource - * @memberof PIXI - * @param {HTMLVideoElement} source - Video element to use. + * @extends PIXI.resources.BaseImageResource + * @memberof PIXI.resources + * @param {HTMLVideoElement|object|string|Array} source - Video element to use. + * @param {object} [options] - Options to use + * @param {boolean} [options.autoLoad=true] - Start loading the video immediately + * @param {boolean} [options.autoPlay=true] - Start playing video immediately + * @param {boolean} [options.crossorigin=true] - Load image using cross origin */ -export default class VideoResource extends TextureResource +export default class VideoResource extends BaseImageResource { - constructor(source) + constructor(source, options) { + options = options || {}; + + if (!(source instanceof HTMLVideoElement)) + { + const videoElement = document.createElement('video'); + + videoElement.setAttribute('webkit-playsinline', ''); + videoElement.setAttribute('playsinline', ''); + + if (typeof source === 'string') + { + source = [source]; + } + + BaseImageResource.crossOrigin(videoElement, (source[0].src || source[0]), options.crossorigin); + + // array of objects or strings + for (let i = 0; i < source.length; ++i) + { + const sourceElement = document.createElement('source'); + + let { src, mime } = source[i]; + + src = src || source[i]; + mime = mime || `video/${src.substr(src.lastIndexOf('.') + 1)}`; + sourceElement.src = src; + sourceElement.type = mime; + + videoElement.appendChild(sourceElement); + } + + // Override the source + source = videoElement; + } + super(source); this._autoUpdate = true; @@ -24,13 +63,43 @@ * @member {boolean} * @default true */ - this.autoPlay = true; + this.autoPlay = options.autoPlay !== false; - this.update = this.update.bind(this); + /** + * Promise when loading + * @member {Promise} + * @private + * @default null + */ + this._load = null; + + /** + * Callback when completed with load. + * @member {function} + * @private + */ + this._resolve = null; + + // Bind for listeners this._onCanPlay = this._onCanPlay.bind(this); + if (options.autoLoad !== false) + { + this.load(); + } + } + + load() + { + if (this._load) + { + return this._load; + } + + const source = this.source; + if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) - && source.width && source.height) + && source.width && source.height) { source.complete = true; } @@ -48,21 +117,21 @@ this._onCanPlay(); } - this.load = new Promise((resolve) => + this._load = new Promise((resolve) => { - this.resolve = resolve; - - if (this.loaded) + if (this.valid) { - this.resolve(this); + resolve(this); + } + else + { + this._resolve = resolve; + + source.load(); } }); - } - update() - { - // TODO - slow down and base on the videos framerate - this.resourceUpdated.emit(); + return this._load; } /** @@ -97,7 +166,7 @@ _onPlayStart() { // Just in case the video has not received its can play even yet.. - if (!this.loaded) + if (!this.valid) { this._onCanPlay(); } @@ -130,53 +199,50 @@ */ _onCanPlay() { - if (this.source) + const { source } = this; + + source.removeEventListener('canplay', this._onCanPlay); + source.removeEventListener('canplaythrough', this._onCanPlay); + + const valid = this.valid; + + this.resize(source.videoWidth, source.videoHeight); + + // prevent multiple loaded dispatches.. + if (!valid && this._resolve) { - this.source.removeEventListener('canplay', this._onCanPlay); - this.source.removeEventListener('canplaythrough', this._onCanPlay); + this._resolve(this); + this._resolve = null; + } - this.width = this.source.videoWidth; - this.height = this.source.videoHeight; - - // prevent multiple loaded dispatches.. - if (!this.loaded) - { - this.loaded = true; - if (this.resolve) - { - this.resolve(this); - } - } - - if (this._isSourcePlaying()) - { - this._onPlayStart(); - } - else if (this.autoPlay) - { - this.source.play(); - } + if (this._isSourcePlaying()) + { + this._onPlayStart(); + } + else if (this.autoPlay) + { + source.play(); } } /** * Destroys this texture - * + * @override */ - destroy() + dispose() { if (this._isAutoUpdating) { Ticker.shared.remove(this.update, this); } - /* - if (this.source && this.source._pixiId) + + if (this.source) { - delete BaseTextureCache[this.source._pixiId]; - delete this.source._pixiId; + this.source.pause(); + this.source.src = ''; + this.source.load(); } -*/ - // super.destroy(); + super.dispose(); } /** @@ -188,7 +254,6 @@ { return this._autoUpdate; } - set autoUpdate(value) // eslint-disable-line require-jsdoc { if (value !== this._autoUpdate) @@ -209,59 +274,20 @@ } /** - * Helper function that creates a new BaseTexture based on the given video element. - * This BaseTexture can then be used to create a texture + * Used to auto-detect the type of resource. * * @static - * @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video. - * @param {string} [videoSrc.src] - One of the source urls for the video - * @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified - * the url's extension will be used as the second part of the mime type. - * @param {number} scaleMode - See {@link PIXI.SCALE_MODES} for possible values - * @return {PIXI.VideoBaseTexture} Newly created VideoBaseTexture + * @param {*} source - The source object + * @param {string} extension - The extension of source, if set + * @return {boolean} `true` if video source */ - static fromUrl(videoSrc, scaleMode) + static test(source, extension) { - const video = document.createElement('video'); - - video.setAttribute('webkit-playsinline', ''); - video.setAttribute('playsinline', ''); - - // array of objects or strings - if (Array.isArray(videoSrc)) - { - for (let i = 0; i < videoSrc.length; ++i) - { - video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime)); - } - } - // single object or string - else - { - video.appendChild(createSource(videoSrc.src || videoSrc, videoSrc.mime)); - } - - video.load(); - - return new VideoResource(video, scaleMode); + return (source instanceof HTMLVideoElement) + || VideoResource.TYPES.indexOf(extension) > -1; } } -function createSource(path, type) -{ - if (!type) - { - type = `video/${path.substr(path.lastIndexOf('.') + 1)}`; - } - - const source = document.createElement('source'); - - source.src = path; - source.type = type; - - return source; -} - /** * List of common video file extensions supported by VideoResource. * @constant diff --git a/packages/core/src/textures/resources/autoDetectResource.js b/packages/core/src/textures/resources/autoDetectResource.js new file mode 100644 index 0000000..8a28176 --- /dev/null +++ b/packages/core/src/textures/resources/autoDetectResource.js @@ -0,0 +1,103 @@ +import ImageResource from './ImageResource'; +import CanvasResource from './CanvasResource'; +import VideoResource from './VideoResource'; +import SVGResource from './SVGResource'; +import BufferResource from './BufferResource'; +import CubeResource from './CubeResource'; +import ArrayResource from './ArrayResource'; + +/** + * Collection of installed resource types, class must extend {@link PIXI.resources.Resource}. + * @example + * class CustomResource extends PIXI.resources.Resource { + * // MUST have source, options constructor signature + * // for auto-detected resources to be created. + * constructor(source, options) { + * super(); + * } + * upload(renderer, baseTexture, glTexture) { + * // upload with GL + * } + * // used to auto-detect resource + * static test(source, extension) { + * return extension === 'xyz'|| source instanceof SomeClass; + * } + * } + * // Install the new resource type + * PIXI.resources.INSTALLED.push(CustomResource); + * + * @name PIXI.resources.INSTALLED + * @type {Array<*>} + * @static + * @readonly + */ +export const INSTALLED = [ + ImageResource, + CanvasResource, + VideoResource, + SVGResource, + BufferResource, + CubeResource, + ArrayResource, +]; + +/** + * Create a resource element from a single source element. This + * auto-detects which type of resource to create. All resources that + * are auto-detectable must have a static `test` method and a constructor + * with the arguments `(source, options?)`. Currently, the supported + * resources for auto-detection include: + * - {@link PIXI.resources.ImageResource} + * - {@link PIXI.resources.CanvasResource} + * - {@link PIXI.resources.VideoResource} + * - {@link PIXI.resources.SVGResource} + * - {@link PIXI.resources.BufferResource} + * @static + * @function PIXI.resources.autoDetectResource + * @param {string|*} source - Resource source, this can be the URL to the resource, + * a typed-array (for BufferResource), HTMLVideoElement, SVG data-uri + * or any other resource that can be auto-detected. If not resource is + * detected, it's assumed to be an ImageResource. + * @param {object} [options] - Pass-through options to use for Resource + * @param {number} [options.width] - BufferResource's width + * @param {number} [options.height] - BufferResource's height + * @param {boolean} [options.autoLoad=true] - Image, SVG and Video flag to start loading + * @param {number} [options.scale=1] - SVG source scale + * @param {boolean} [options.createBitmap=true] - Image option to create Bitmap object + * @param {boolean} [options.crossorigin=true] - Image and Video option to set crossOrigin + * @return {PIXI.resources.Resource} The created resource. + */ +export function autoDetectResource(source, options) +{ + if (!source) + { + return null; + } + + let extension = ''; + + if (typeof source === 'string') + { + // search for file extension: period, 3-4 chars, then ?, # or EOL + const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); + + if (result) + { + extension = result[1].toLowerCase(); + } + } + + for (let i = INSTALLED.length - 1; i >= 0; --i) + { + const ResourcePlugin = INSTALLED[i]; + + if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) + { + return new ResourcePlugin(source, options); + } + } + + // When in doubt: probably an image + // might be appropriate to throw an error or return null + return new ImageResource(source, options); +} diff --git a/packages/core/src/textures/resources/createResource.js b/packages/core/src/textures/resources/createResource.js deleted file mode 100644 index ce437b2..0000000 --- a/packages/core/src/textures/resources/createResource.js +++ /dev/null @@ -1,44 +0,0 @@ -import ImageResource from './ImageResource'; -import SVGResource from './SVGResource'; -import CanvasResource from './CanvasResource'; -import VideoResource from './VideoResource'; - -export default function createResource(source) -{ - if (typeof source === 'string') - { - // search for file extension: period, 3-4 chars, then ?, # or EOL - const result = (/\.(\w{3,4})(?:$|\?|#)/i).exec(source); - - if (result) - { - const extension = result[1].toLowerCase(); - - if (VideoResource.TYPES.indexOf(extension) > -1) - { - return new VideoResource.fromUrl(source); - } - else if (SVGResource.TYPES.indexOf(extension) > -1) - { - return SVGResource.from(source); - } - } - - // probably an image! - return ImageResource.from(source); - } - else if (source instanceof HTMLImageElement) - { - return new ImageResource(source); - } - else if (source instanceof HTMLCanvasElement) - { - return new CanvasResource(source); - } - else if (source instanceof HTMLVideoElement) - { - return new VideoResource(source); - } - - return source; -} diff --git a/packages/core/src/textures/resources/index.js b/packages/core/src/textures/resources/index.js new file mode 100644 index 0000000..d2cb42c --- /dev/null +++ b/packages/core/src/textures/resources/index.js @@ -0,0 +1,20 @@ +/** + * Collection of base resource types supported by PixiJS. + * Resources are used by {@link PIXI.BaseTexture} to handle different media types + * such as images, video, SVG graphics, etc. In most use-cases, you should not + * instantiate the resources directly. The easy thing is to use + * {@link PIXI.BaseTexture.from}. + * @example + * const baseTexture = PIXI.BaseTexture.from('path/to/image.jpg'); + * @namespace PIXI.resources + */ +export * from './autoDetectResource'; +export { default as Resource } from './Resource'; +export { default as ArrayResource } from './ArrayResource'; +export { default as BaseImageResource } from './BaseImageResource'; +export { default as BufferResource } from './BufferResource'; +export { default as CanvasResource } from './CanvasResource'; +export { default as CubeResource } from './CubeResource'; +export { default as ImageResource } from './ImageResource'; +export { default as SVGResource } from './SVGResource'; +export { default as VideoResource } from './VideoResource'; diff --git a/packages/core/test/ArrayResource.js b/packages/core/test/ArrayResource.js new file mode 100644 index 0000000..c5c2502 --- /dev/null +++ b/packages/core/test/ArrayResource.js @@ -0,0 +1,65 @@ +const { resources } = require('../'); +const { ArrayResource, ImageResource } = resources; +const { join } = require('path'); + +describe('PIXI.resources.ArrayResource', function () +{ + before(function () + { + this.basePath = join(__dirname, 'resources'); + this.imageUrl = join(this.basePath, 'slug.png'); + }); + + it('should create new resource by length', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + + resource.destroy(); + expect(resource.destroyed).to.be.true; + }); + + it('should error on out of bound', function () + { + const resource = new ArrayResource(5, { width: 100, height: 100 }); + const image = new ImageResource(this.imageUrl); + + expect(() => resource.addResourceAt(image, 10)).to.throw(Error, /out of bounds/); + + resource.destroy(); + }); + + it('should load array of URL resources', function () + { + const images = [ + this.imageUrl, + this.imageUrl, + this.imageUrl, + this.imageUrl, + ]; + + const resource = new ArrayResource(images, { + width: 100, + height: 100, + autoLoad: false, + }); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(baseTexture.setRealSize.calledOnce).to.be.true; + for (let i = 0; i < images.length; i++) + { + const item = resource.items[i].resource; + + expect(item.valid).to.be.true; + expect(item.width).to.equal(100); + expect(item.height).to.equal(100); + } + resource.unbind(baseTexture); + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/BaseTexture.js b/packages/core/test/BaseTexture.js index e140bd2..51879dd 100644 --- a/packages/core/test/BaseTexture.js +++ b/packages/core/test/BaseTexture.js @@ -1,5 +1,6 @@ const { BaseTextureCache, TextureCache } = require('@pixi/utils'); -const { BaseTexture, Texture, ImageResource } = require('../'); +const { BaseTexture, Texture, resources } = require('../'); +const { ImageResource } = resources; const URL = 'foo.png'; const NAME = 'foo'; @@ -40,7 +41,7 @@ cleanCache(); const canvas = document.createElement('canvas'); - const baseTexture = BaseTexture.fromCanvas(canvas); + const baseTexture = BaseTexture.from(canvas); const _pixiId = canvas._pixiId; expect(baseTexture.textureCacheIds.indexOf(_pixiId)).to.equal(0); @@ -113,14 +114,64 @@ baseTexture.destroy(); }); + it('should update width and height', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 100; + + const baseTexture = BaseTexture.from(canvas); + + expect(baseTexture.width).to.equal(canvas.width); + expect(baseTexture.height).to.equal(canvas.height); + + baseTexture.destroy(); + }); + it('should set source.crossOrigin to anonymous if explicitly set', function () { cleanCache(); - const imageResource = ImageResource.from(URL, true); + const imageResource = new ImageResource(URL, { + crossorigin: true, + }); const baseTexture = new BaseTexture(imageResource); expect(baseTexture.resource.source.crossOrigin).to.equal('anonymous'); + + baseTexture.destroy(); + imageResource.destroy(); + }); + + it('should not destroy externally created resources', function () + { + cleanCache(); + + const imageResource = new ImageResource(URL); + const baseTexture = new BaseTexture(imageResource); + + baseTexture.destroy(); + + expect(baseTexture.destroyed).to.be.true; + expect(imageResource.destroyed).to.be.false; + + imageResource.destroy(); + + expect(imageResource.destroyed).to.be.true; + }); + + it('should destroy internally created resources', function () + { + cleanCache(); + + const baseTexture = new BaseTexture(URL); + const { resource } = baseTexture; + + baseTexture.destroy(); + + expect(resource.destroyed).to.be.true; + expect(baseTexture.destroyed).to.be.true; }); }); diff --git a/packages/core/test/CanvasResource.js b/packages/core/test/CanvasResource.js new file mode 100644 index 0000000..f6a492f --- /dev/null +++ b/packages/core/test/CanvasResource.js @@ -0,0 +1,66 @@ +const { resources } = require('../'); +const { CanvasResource } = resources; + +describe('PIXI.resources.CanvasResource', function () +{ + it('should create new dimension-less resource', function () + { + const canvas = document.createElement('canvas'); + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(canvas.width); + expect(resource.height).to.equal(canvas.height); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should create new valid resource', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 100; + canvas.height = 200; + + const resource = new CanvasResource(canvas); + + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(200); + expect(resource.valid).to.be.true; + + resource.destroy(); + }); + + it('should fire resize event on bind', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { setRealSize: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.setRealSize.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); + + it('should fire manual update event', function () + { + const canvas = document.createElement('canvas'); + const resource = new CanvasResource(canvas); + const baseTexture = { update: sinon.stub() }; + + resource.bind(baseTexture); + + expect(baseTexture.update.called).to.be.false; + + resource.update(); + + expect(baseTexture.update.calledOnce).to.be.true; + + resource.unbind(baseTexture); + resource.destroy(); + }); +}); diff --git a/packages/core/test/CubeResource.js b/packages/core/test/CubeResource.js new file mode 100644 index 0000000..cb58d12 --- /dev/null +++ b/packages/core/test/CubeResource.js @@ -0,0 +1,14 @@ +const { resources } = require('../'); +const { CubeResource } = resources; + +describe('PIXI.resources.CubeResource', function () +{ + it('should create invalid length resource', function () + { + expect(() => + { + // eslint-disable-next-line no-new + new CubeResource(8); + }).to.throw(Error, /invalid length/i); + }); +}); diff --git a/packages/core/test/ImageResource.js b/packages/core/test/ImageResource.js new file mode 100644 index 0000000..e22c2a9 --- /dev/null +++ b/packages/core/test/ImageResource.js @@ -0,0 +1,88 @@ +const { resources } = require('../'); +const { ImageResource } = resources; +const path = require('path'); + +describe('PIXI.resources.ImageResource', function () +{ + before(function () + { + this.slugUrl = path.resolve(__dirname, 'resources', 'slug.png'); + }); + + it('should create new dimension-less resource', function () + { + const image = new Image(); + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(''); + + resource.destroy(); + }); + + it('should destroy resource multiple times', function () + { + const resource = new ImageResource(new Image()); + + resource.destroy(); + resource.destroy(); + }); + + it('should create new valid resource from HTMLImageElement', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.url).to.equal(`file://${this.slugUrl}`); + + resource.destroy(); + }); + + it('should handle the loaded event with createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { autoLoad: false }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.instanceof(ImageBitmap); + }); + }); + + it('should handle the loaded event with no createBitmapImage', function () + { + const image = new Image(); + + image.src = this.slugUrl; + + const resource = new ImageResource(image, { + autoLoad: false, + createBitmap: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + expect(resource.valid).to.be.true; + expect(resource.bitmap).to.be.null; + }); + }); +}); diff --git a/packages/core/test/SVGResource.js b/packages/core/test/SVGResource.js index d8e5147..713a0e3 100644 --- a/packages/core/test/SVGResource.js +++ b/packages/core/test/SVGResource.js @@ -1,7 +1,64 @@ -const { SVGResource } = require('../'); +const { resources } = require('../'); +const { SVGResource } = resources; +const fs = require('fs'); +const path = require('path'); -describe('PIXI.SVGResource', function () +describe('PIXI.resources.SVGResource', function () { + before(function () + { + this.resources = path.join(__dirname, 'resources'); + }); + + describe('constructor', function () + { + it('should create new resource from data-uri', function (done) + { + const url = path.join(this.resources, 'svg-base64.txt'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(16); + expect(resource.height).to.equal(16); + + done(); + }); + }); + + it('should create resource from SVG URL', function (done) + { + const resource = new SVGResource( + path.join(this.resources, 'heart.svg'), + { autoLoad: false } + ); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + + it('should create resource from inline SVG', function (done) + { + const url = path.join(this.resources, 'heart.svg'); + const buffer = fs.readFileSync(url, 'utf8'); + const resource = new SVGResource(buffer, { autoLoad: false }); + + resource.load().then(function () + { + expect(resource.width).to.equal(100); + expect(resource.height).to.equal(100); + + done(); + }); + }); + }); + describe('getSize', function () { it('should exist', function () diff --git a/packages/core/test/VideoResource.js b/packages/core/test/VideoResource.js new file mode 100644 index 0000000..3b90eb4 --- /dev/null +++ b/packages/core/test/VideoResource.js @@ -0,0 +1,40 @@ +const { resources } = require('../'); +const { VideoResource } = resources; +const path = require('path'); + +describe('PIXI.resources.VideoResource', function () +{ + before(function () + { + this.videoUrl = path.resolve(__dirname, 'resources', 'small.mp4'); + }); + + it('should create new resource', function () + { + const resource = new VideoResource(this.videoUrl, { autoLoad: false }); + + expect(resource.width).to.equal(0); + expect(resource.height).to.equal(0); + expect(resource.valid).to.be.false; + expect(resource.source).to.be.instanceof(HTMLVideoElement); + + resource.destroy(); + }); + + it('should load new resource', function () + { + const resource = new VideoResource(this.videoUrl, { + autoLoad: false, + autoPlay: false, + }); + + return resource.load().then((res) => + { + expect(res).to.equal(resource); + expect(res.width).to.equal(560); + expect(res.height).to.equal(320); + expect(res.valid).to.be.true; + resource.destroy(); + }); + }); +}); diff --git a/packages/core/test/autoDetectResource.js b/packages/core/test/autoDetectResource.js new file mode 100644 index 0000000..d11e3fe --- /dev/null +++ b/packages/core/test/autoDetectResource.js @@ -0,0 +1,83 @@ +const { resources } = require('../'); +const { + autoDetectResource, + INSTALLED, + CanvasResource, + ImageResource, + VideoResource, + SVGResource } = resources; + +describe('PIXI.resources.autoDetectResource', function () +{ + it('should have api', function () + { + expect(autoDetectResource).to.be.a.function; + }); + + it('should have installed resources', function () + { + expect(INSTALLED).to.be.an.array; + expect(INSTALLED.length).to.equal(7); + }); + + it('should auto-detect canvas element', function () + { + const canvas = document.createElement('canvas'); + + canvas.width = 200; + canvas.height = 100; + + const resource = autoDetectResource(canvas); + + expect(resource).is.instanceOf(CanvasResource); + expect(resource.width).to.equal(200); + expect(resource.height).to.equal(100); + }); + + it('should auto-detect video element', function () + { + const video = document.createElement('video'); + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should auto-detect image element', function () + { + const img = new Image(); + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect image string', function () + { + const img = 'foo.png'; + const resource = autoDetectResource(img); + + expect(resource).is.instanceOf(ImageResource); + }); + + it('should auto-detect svg string', function () + { + const svg = 'foo.svg'; + const resource = autoDetectResource(svg); + + expect(resource).is.instanceOf(SVGResource); + }); + + it('should auto-detect video Url', function () + { + const video = 'foo.mp4'; + const resource = autoDetectResource(video); + + expect(resource).is.instanceOf(VideoResource); + }); + + it('should pass null', function () + { + const resource = autoDetectResource(null); + + expect(resource).to.equal(null); + }); +}); diff --git a/packages/core/test/index.js b/packages/core/test/index.js index fac5c82..4268251 100644 --- a/packages/core/test/index.js +++ b/packages/core/test/index.js @@ -1,4 +1,10 @@ require('./BaseTexture'); require('./Texture'); require('./Renderer'); +require('./CanvasResource'); +require('./ImageResource'); require('./SVGResource'); +require('./VideoResource'); +require('./ArrayResource'); +require('./autoDetectResource'); +require('./CubeResource'); diff --git a/packages/core/test/resources/heart.svg b/packages/core/test/resources/heart.svg new file mode 100644 index 0000000..6aed10f --- /dev/null +++ b/packages/core/test/resources/heart.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/core/test/resources/slug.png b/packages/core/test/resources/slug.png new file mode 100644 index 0000000..9b4569a --- /dev/null +++ b/packages/core/test/resources/slug.png Binary files differ diff --git a/packages/core/test/resources/small.mp4 b/packages/core/test/resources/small.mp4 new file mode 100644 index 0000000..b8637a3 --- /dev/null +++ b/packages/core/test/resources/small.mp4 Binary files differ diff --git a/packages/core/test/resources/svg-base64.txt b/packages/core/test/resources/svg-base64.txt new file mode 100644 index 0000000..7b3bc4a --- /dev/null +++ b/packages/core/test/resources/svg-base64.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNi4wLjQsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkNhcGFfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTYgMTYiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHBhdGggZD0iTTgsMEMzLjU4MiwwLDAsMy41ODIsMCw4czMuNTgyLDgsOCw4czgtMy41ODIsOC04UzEyLjQxOCwwLDgsMHogTTksMTJjMCwwLjU1Mi0wLjQ0OCwxLTEsMXMtMS0wLjQ0OC0xLTFWNw0KCWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxVjEyeiBNOCw1LjAxNmMtMC41NTIsMC0xLTAuNDQ4LTEtMWMwLTAuNTUyLDAuNDQ4LTEsMS0xczEsMC40NDgsMSwxQzksNC41NjgsOC41NTIsNS4wMTYsOCw1LjAxNnoNCgkiLz4NCjwvc3ZnPg0K1422f94715e8d9b67004ad32568deab3 \ No newline at end of file diff --git a/packages/polyfill/package.json b/packages/polyfill/package.json index d841b6c..8b4b579 100644 --- a/packages/polyfill/package.json +++ b/packages/polyfill/package.json @@ -22,9 +22,10 @@ "lib" ], "dependencies": { + "es6-promise-polyfill": "^1.2.0", "object-assign": "^4.0.1" }, "devDependencies": { "floss": "^2.1.3" } -} \ No newline at end of file +} diff --git a/packages/polyfill/src/Promise.js b/packages/polyfill/src/Promise.js new file mode 100644 index 0000000..9ea91f0 --- /dev/null +++ b/packages/polyfill/src/Promise.js @@ -0,0 +1,7 @@ +import { Polyfill } from 'es6-promise-polyfill'; + +// Support for IE 9 - 11 which does not include Promises +if (!window.Promise) +{ + window.Promise = Polyfill; +} diff --git a/packages/polyfill/src/index.js b/packages/polyfill/src/index.js index d963dee..fb2ca08 100644 --- a/packages/polyfill/src/index.js +++ b/packages/polyfill/src/index.js @@ -1,3 +1,4 @@ +import './Promise'; import './Object.assign'; import './requestAnimationFrame'; import './Math.sign'; @@ -21,3 +22,13 @@ { window.Uint16Array = Array; } + +if (!window.Uint8Array) +{ + window.Uint8Array = Array; +} + +if (!window.Int32Array) +{ + window.Int32Array = Array; +} diff --git a/packages/settings/src/settings.js b/packages/settings/src/settings.js index 4de6795..3f4132e 100644 --- a/packages/settings/src/settings.js +++ b/packages/settings/src/settings.js @@ -189,4 +189,15 @@ * @type {boolean} */ CAN_UPLOAD_SAME_BUFFER: canUploadSameBuffer(), + + /** + * Enables bitmap creation before image load + * + * @static + * @constant + * @memberof PIXI + * @type {boolean} + * @default true + */ + CREATE_IMAGE_BITMAP: true, }; diff --git a/packages/spritesheet/src/Spritesheet.js b/packages/spritesheet/src/Spritesheet.js index c314691..4f66a77 100644 --- a/packages/spritesheet/src/Spritesheet.js +++ b/packages/spritesheet/src/Spritesheet.js @@ -113,8 +113,7 @@ // For non-1 resolutions, update baseTexture if (resolution !== 1) { - this.baseTexture.resolution = resolution; - this.baseTexture.updateResolution(); + this.baseTexture.setResolution(resolution); } return resolution; diff --git a/packages/spritesheet/test/Spritesheet.js b/packages/spritesheet/test/Spritesheet.js index 35d8688..17ea65f 100644 --- a/packages/spritesheet/test/Spritesheet.js +++ b/packages/spritesheet/test/Spritesheet.js @@ -73,7 +73,7 @@ it('should create instance with BaseTexture source scale', function (done) { const data = require(path.resolve(this.resources, 'building1.json')); // eslint-disable-line global-require - const baseTexture = new BaseTexture.fromImage(data.meta.image, undefined, undefined, 1.5); + const baseTexture = BaseTexture.from(data.meta.image);// , undefined, undefined, 1.5); const spritesheet = new Spritesheet(baseTexture, data); expect(data).to.be.an.object; @@ -92,7 +92,7 @@ image.src = path.join(this.resources, data.meta.image); image.onload = () => { - const baseTexture = new BaseTexture(image, null, 1); + const baseTexture = new BaseTexture(image, { resolution: 1 }); const spritesheet = new Spritesheet(baseTexture, data, uri); expect(data).to.be.an.object; diff --git a/packages/text-bitmap/test/BitmapFontLoader.js b/packages/text-bitmap/test/BitmapFontLoader.js index f0a6db5..8cb514e 100644 --- a/packages/text-bitmap/test/BitmapFontLoader.js +++ b/packages/text-bitmap/test/BitmapFontLoader.js @@ -169,7 +169,11 @@ it('should properly register SCALED bitmap font', function (done) { - const texture = new Texture(new BaseTexture(this.fontScaledImage, null, 0.5)); + const baseTexture = new BaseTexture(this.fontScaledImage); + + baseTexture.setResolution(0.5); + + const texture = new Texture(baseTexture); const font = BitmapText.registerFont(this.fontScaledXML, texture); expect(font).to.be.an.object; diff --git a/packages/text/src/Text.js b/packages/text/src/Text.js index 1cb4a1c..169935f 100644 --- a/packages/text/src/Text.js +++ b/packages/text/src/Text.js @@ -327,12 +327,6 @@ const padding = style.trim ? 0 : style.padding; const baseTexture = texture.baseTexture; - baseTexture.hasLoaded = true; - baseTexture.resolution = this.resolution; - - baseTexture.width = canvas.width / this.resolution; - baseTexture.height = canvas.height / this.resolution; - texture.trim.width = texture._frame.width = canvas.width / this.resolution; texture.trim.height = texture._frame.height = canvas.height / this.resolution; texture.trim.x = -padding; @@ -344,7 +338,7 @@ // call sprite onTextureUpdate to update scale if _width or _height were set this._onTextureUpdate(); - texture.baseTexture.update();// emit('update', texture.baseTexture); + baseTexture.setRealSize(canvas.width, canvas.height, this.resolution); this.dirty = false; }