diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index e1d88e7..8e61919 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -11,7 +11,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class MeshRenderer extends core.ObjectRenderer { +export default class MeshRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index e1d88e7..8e61919 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -11,7 +11,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class MeshRenderer extends core.ObjectRenderer { +export default class MeshRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js index 7fd0b70..265c46c 100644 --- a/src/prepare/limiters/CountLimiter.js +++ b/src/prepare/limiters/CountLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class CountLimiter { +export default class CountLimiter +{ /** * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. */ diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index e1d88e7..8e61919 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -11,7 +11,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class MeshRenderer extends core.ObjectRenderer { +export default class MeshRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js index 7fd0b70..265c46c 100644 --- a/src/prepare/limiters/CountLimiter.js +++ b/src/prepare/limiters/CountLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class CountLimiter { +export default class CountLimiter +{ /** * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. */ diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js index 8908aba..5f40686 100644 --- a/src/prepare/limiters/TimeLimiter.js +++ b/src/prepare/limiters/TimeLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class TimeLimiter { +export default class TimeLimiter +{ /** * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. */ diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index e1d88e7..8e61919 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -11,7 +11,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class MeshRenderer extends core.ObjectRenderer { +export default class MeshRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js index 7fd0b70..265c46c 100644 --- a/src/prepare/limiters/CountLimiter.js +++ b/src/prepare/limiters/CountLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class CountLimiter { +export default class CountLimiter +{ /** * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. */ diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js index 8908aba..5f40686 100644 --- a/src/prepare/limiters/TimeLimiter.js +++ b/src/prepare/limiters/TimeLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class TimeLimiter { +export default class TimeLimiter +{ /** * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. */ diff --git a/test/core/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index e1d88e7..8e61919 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -11,7 +11,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class MeshRenderer extends core.ObjectRenderer { +export default class MeshRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js index 7fd0b70..265c46c 100644 --- a/src/prepare/limiters/CountLimiter.js +++ b/src/prepare/limiters/CountLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class CountLimiter { +export default class CountLimiter +{ /** * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. */ diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js index 8908aba..5f40686 100644 --- a/src/prepare/limiters/TimeLimiter.js +++ b/src/prepare/limiters/TimeLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class TimeLimiter { +export default class TimeLimiter +{ /** * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. */ diff --git a/test/core/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/src/core/graphics/webgl/utils/buildCircle.js b/src/core/graphics/webgl/utils/buildCircle.js index 0400d13..abf5d02 100644 --- a/src/core/graphics/webgl/utils/buildCircle.js +++ b/src/core/graphics/webgl/utils/buildCircle.js @@ -33,6 +33,11 @@ height = circleData.height; } + if (width === 0 || height === 0) + { + return; + } + const totalSegs = Math.floor(30 * Math.sqrt(circleData.radius)) || Math.floor(15 * Math.sqrt(circleData.width + circleData.height)); diff --git a/src/core/renderers/webgl/managers/FilterManager.js b/src/core/renderers/webgl/managers/FilterManager.js index 64f5d9d..dd7419c 100644 --- a/src/core/renderers/webgl/managers/FilterManager.js +++ b/src/core/renderers/webgl/managers/FilterManager.js @@ -184,7 +184,7 @@ flop = t; } - filters[i].apply(this, flip, lastState.renderTarget, true); + filters[i].apply(this, flip, lastState.renderTarget, false); this.freePotRenderTarget(flip); this.freePotRenderTarget(flop); @@ -289,10 +289,11 @@ let textureCount = 1; let currentState; - if (shader.uniforms.data.filterArea) + if (uniforms.filterArea) { currentState = this.filterData.stack[this.filterData.index]; - const filterArea = shader.uniforms.filterArea; + + const filterArea = uniforms.filterArea; filterArea[0] = currentState.renderTarget.size.width; filterArea[1] = currentState.renderTarget.size.height; @@ -304,11 +305,11 @@ // use this to clamp displaced texture coords so they belong to filterArea // see displacementFilter fragment shader for an example - if (shader.uniforms.data.filterClamp) + if (uniforms.filterClamp) { currentState = this.filterData.stack[this.filterData.index]; - const filterClamp = shader.uniforms.filterClamp; + const filterClamp = uniforms.filterClamp; filterClamp[0] = 0; filterClamp[1] = 0; @@ -483,7 +484,7 @@ */ destroy() { - this.shaderCache = []; + this.shaderCache = {}; this.emptyPool(); } diff --git a/src/core/sprites/webgl/SpriteRenderer.js b/src/core/sprites/webgl/SpriteRenderer.js index 0da2452..f7cc1a0 100644 --- a/src/core/sprites/webgl/SpriteRenderer.js +++ b/src/core/sprites/webgl/SpriteRenderer.js @@ -137,8 +137,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[i], shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[i], shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (shader.attributes.aTextureId) + { + this.vaos[i].addAttribute(this.vertexBuffers[i], shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } @@ -385,8 +389,12 @@ .addIndex(this.indexBuffer) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4) - .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + .addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); + + if (this.shader.attributes.aTextureId) + { + this.vaos[this.vertexCount].addAttribute(this.vertexBuffers[this.vertexCount], this.shader.attributes.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); + } /* eslint-enable max-len */ } diff --git a/src/core/text/Text.js b/src/core/text/Text.js index 013625a..18fb723 100644 --- a/src/core/text/Text.js +++ b/src/core/text/Text.js @@ -6,6 +6,7 @@ import { TEXT_GRADIENT } from '../const'; import settings from '../settings'; import TextStyle from './TextStyle'; +import trimCanvas from '../utils/trimCanvas'; const defaultDestroyOptions = { texture: true, @@ -57,7 +58,7 @@ /** * The canvas 2d context that everything is drawn with - * @member {HTMLCanvasElement} + * @member {CanvasRenderingContext2D} */ this.context = this.canvas.getContext('2d'); @@ -189,10 +190,11 @@ if (style.dropShadow) { + this.context.shadowBlur = style.dropShadowBlur; + if (style.dropShadowBlur > 0) { this.context.shadowColor = style.dropShadowColor; - this.context.shadowBlur = style.dropShadowBlur; } else { @@ -240,6 +242,9 @@ // set canvas text styles this.context.fillStyle = this._generateFillStyle(style, lines); + // remove blur if set for the drop shadow + this.context.shadowBlur = 0; + // draw lines line by line for (let i = 0; i < lines.length; i++) { @@ -326,6 +331,15 @@ */ updateTexture() { + if (this._style.trim) + { + const trimmed = trimCanvas(this.canvas); + + this.canvas.width = trimmed.width; + this.canvas.height = trimmed.height; + this.context.putImageData(trimmed.data, 0, 0); + } + const texture = this._texture; const style = this._style; @@ -475,6 +489,19 @@ } /** + * Gets the local bounds of the text object. + * + * @param {Rectangle} rect - The output rectangle. + * @return {Rectangle} The bounds. + */ + getLocalBounds(rect) + { + this.updateText(true); + + return super.getLocalBounds.call(this, rect); + } + + /** * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. */ _calculateBounds() @@ -525,6 +552,29 @@ const width = this.canvas.width / this.resolution; const height = this.canvas.height / this.resolution; + // make a copy of the style settings, so we can manipulate them later + const fill = style.fill.slice(); + const fillGradientStops = style.fillGradientStops.slice(); + + // wanting to evenly distribute the fills. So an array of 4 colours should give fills of 0.25, 0.5 and 0.75 + if (!fillGradientStops.length) + { + const lengthPlus1 = fill.length + 1; + + for (let i = 1; i < lengthPlus1; ++i) + { + fillGradientStops.push(i / lengthPlus1); + } + } + + // stop the bleeding of the last gradient on the line above to the top gradient of the this line + // by hard defining the first gradient colour at point 0, and last gradient colour at point 1 + fill.unshift(style.fill[0]); + fillGradientStops.unshift(0); + + fill.push(style.fill[style.fill.length - 1]); + fillGradientStops.push(1); + if (style.fillGradientType === TEXT_GRADIENT.LINEAR_VERTICAL) { // start the gradient at the top center of the canvas, and end at the bottom middle of the canvas @@ -532,15 +582,22 @@ // we need to repeat the gradient so that each individual line of text has the same vertical gradient effect // ['#FF0000', '#00FF00', '#0000FF'] over 2 lines would create stops at 0.125, 0.25, 0.375, 0.625, 0.75, 0.875 - totalIterations = (style.fill.length + 1) * lines.length; + totalIterations = (fill.length + 1) * lines.length; currentIteration = 0; for (let i = 0; i < lines.length; i++) { currentIteration += 1; - for (let j = 0; j < style.fill.length; j++) + for (let j = 0; j < fill.length; j++) { - stop = (currentIteration / totalIterations); - gradient.addColorStop(stop, style.fill[j]); + if (fillGradientStops[j]) + { + stop = (fillGradientStops[j] / lines.length) + (i / lines.length); + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[j]); currentIteration++; } } @@ -552,13 +609,20 @@ // can just evenly space out the gradients in this case, as multiple lines makes no difference // to an even left to right gradient - totalIterations = style.fill.length + 1; + totalIterations = fill.length + 1; currentIteration = 1; - for (let i = 0; i < style.fill.length; i++) + for (let i = 0; i < fill.length; i++) { - stop = currentIteration / totalIterations; - gradient.addColorStop(stop, style.fill[i]); + if (fillGradientStops[i]) + { + stop = fillGradientStops[i]; + } + else + { + stop = currentIteration / totalIterations; + } + gradient.addColorStop(stop, fill[i]); currentIteration++; } } @@ -569,7 +633,7 @@ /** * Destroys this text object. * Note* Unlike a Sprite, a Text object will automatically destroy its baseTexture and texture as - * the majorety of the time the texture will not be shared with any other Sprites. + * the majority of the time the texture will not be shared with any other Sprites. * * @param {object|boolean} [options] - Options parameter. A boolean will act as if all options * have been set to that value @@ -680,7 +744,7 @@ set text(text) // eslint-disable-line require-jsdoc { - text = String(text || ' '); + text = String(text === '' || text === null || text === undefined ? ' ' : text); if (this._text === text) { diff --git a/src/core/text/TextStyle.js b/src/core/text/TextStyle.js index 3b25528..eec2820 100644 --- a/src/core/text/TextStyle.js +++ b/src/core/text/TextStyle.js @@ -14,6 +14,7 @@ dropShadowDistance: 5, fill: 'black', fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, + fillGradientStops: [], fontFamily: 'Arial', fontSize: 26, fontStyle: 'normal', @@ -27,6 +28,7 @@ stroke: 'black', strokeThickness: 0, textBaseline: 'alphabetic', + trim: false, wordWrap: false, wordWrapWidth: 100, }; @@ -55,8 +57,10 @@ * fillstyle that will be used on the text e.g 'red', '#00FF00'. Can be an array to create a gradient * eg ['#000000','#FFFFFF'] * {@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle|MDN} - * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fills styles are - * supplied, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} for possible values + * @param {number} [style.fillGradientType=PIXI.TEXT_GRADIENT.LINEAR_VERTICAL] - If fill is an array of colours + * to create a gradient, this can change the type/direction of the gradient. See {@link PIXI.TEXT_GRADIENT} + * @param {number[]} [style.fillGradientStops] - If fill is an array of colours to create a gradient, this array can set + * the stop points (numbers between 0 and 1) for the color, overriding the default behaviour of evenly spacing them. * @param {string|string[]} [style.fontFamily='Arial'] - The font family * @param {number|string} [style.fontSize=26] - The font size (as a number it converts to px, but as a string, * equivalents are '26px','20pt','160%' or '1.6em') @@ -76,6 +80,7 @@ * e.g 'blue', '#FCFF00' * @param {number} [style.strokeThickness=0] - A number that represents the thickness of the stroke. * Default is 0 (no stroke) + * @param {boolean} [style.trim=false] - Trim transparent borders * @param {string} [style.textBaseline='alphabetic'] - The baseline of the text that is rendered. * @param {boolean} [style.wordWrap=false] - Indicates if word wrap should be used * @param {number} [style.wordWrapWidth=100] - The width at which text will wrap, it needs wordWrap to be set to true @@ -232,6 +237,19 @@ } } + get fillGradientStops() + { + return this._fillGradientStops; + } + set fillGradientStops(fillGradientStops) + { + if (!areArraysEqual(this._fillGradientStops,fillGradientStops)) + { + this._fillGradientStops = fillGradientStops; + this.styleID++; + } + } + get fontFamily() { return this._fontFamily; @@ -402,6 +420,19 @@ } } + get trim() + { + return this._trim; + } + set trim(trim) + { + if (this._trim !== trim) + { + this._trim = trim; + this.styleID++; + } + } + get wordWrap() { return this._wordWrap; @@ -475,3 +506,34 @@ return color; } } + +/** + * Utility function to convert hexadecimal colors to strings, and simply return the color if it's a string. + * This version can also convert array of colors + * + * @param {Array} array1 First array to compared + * @param {Array} array1 Second array to compare + * @return {boolean} Do the arrays contain the same values in the same order + */ +function areArraysEqual(array1, array2) +{ + if (!Array.isArray(array1) || !Array.isArray(array2)) + { + return false; + } + + if (array1.length !== array2.length) + { + return false; + } + + for (let i = 0; i < array1.length; ++i) + { + if (array1[i] !== array2[i]) + { + return false; + } + } + + return true; +} diff --git a/src/core/textures/Texture.js b/src/core/textures/Texture.js index ca31456..6403d4f 100644 --- a/src/core/textures/Texture.js +++ b/src/core/textures/Texture.js @@ -459,7 +459,9 @@ if (frame.x + frame.width > this.baseTexture.width || frame.y + frame.height > this.baseTexture.height) { - throw new Error(`Texture Error: frame does not fit inside the base Texture dimensions ${this}`); + throw new Error('Texture Error: frame does not fit inside the base Texture dimensions: ' + + `X: ${frame.x} + ${frame.width} > ${this.baseTexture.width} ` + + `Y: ${frame.y} + ${frame.height} > ${this.baseTexture.height}`); } // this.valid = frame && frame.width && frame.height && this.baseTexture.source && this.baseTexture.hasLoaded; diff --git a/src/core/utils/trimCanvas.js b/src/core/utils/trimCanvas.js new file mode 100644 index 0000000..5af6de0 --- /dev/null +++ b/src/core/utils/trimCanvas.js @@ -0,0 +1,83 @@ +/** + * Trim transparent borders from a canvas + * + * @memberof PIXI + * @function trimCanvas + * @private + * @param {HTMLCanvasElement} canvas - the canvas to trim + * @returns {object} Trim data + */ +export default function trimCanvas(canvas) +{ + // https://gist.github.com/remy/784508 + + let width = canvas.width; + let height = canvas.height; + + const context = canvas.getContext('2d'); + const imageData = context.getImageData(0, 0, width, height); + const pixels = imageData.data; + const len = pixels.length; + + const bound = { + top: null, + left: null, + right: null, + bottom: null, + }; + let i; + let x; + let y; + + for (i = 0; i < len; i += 4) + { + if (pixels[i + 3] !== 0) + { + x = (i / 4) % width; + y = ~~((i / 4) / width); + + if (bound.top === null) + { + bound.top = y; + } + + if (bound.left === null) + { + bound.left = x; + } + else if (x < bound.left) + { + bound.left = x; + } + + if (bound.right === null) + { + bound.right = x + 1; + } + else if (bound.right < x) + { + bound.right = x + 1; + } + + if (bound.bottom === null) + { + bound.bottom = y; + } + else if (bound.bottom < y) + { + bound.bottom = y; + } + } + } + + width = bound.right - bound.left; + height = bound.bottom - bound.top + 1; + + const data = context.getImageData(bound.left, bound.top, width, height); + + return { + height, + width, + data, + }; +} diff --git a/src/extras/TextureTransform.js b/src/extras/TextureTransform.js index 58623b1..78e6e40 100644 --- a/src/extras/TextureTransform.js +++ b/src/extras/TextureTransform.js @@ -8,7 +8,8 @@ * @class * @memberof PIXI.extras */ -export default class TextureTransform { +export default class TextureTransform +{ /** * * @param {PIXI.Texture} texture observed texture diff --git a/src/extras/TilingSprite.js b/src/extras/TilingSprite.js index f90d248..fe82451 100644 --- a/src/extras/TilingSprite.js +++ b/src/extras/TilingSprite.js @@ -220,20 +220,33 @@ transform.tx * resolution, transform.ty * resolution); - // TODO - this should be rolled into the setTransform above.. - context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); - - context.translate(modX + (this.anchor.x * -this._width), - modY + (this.anchor.y * -this._height)); - renderer.setBlendMode(this.blendMode); // fill the pattern! context.fillStyle = this._canvasPattern; - context.fillRect(-modX, - -modY, - this._width / this.tileScale.x * baseTextureResolution, - this._height / this.tileScale.y * baseTextureResolution); + + // TODO - this should be rolled into the setTransform above.. + context.scale(this.tileScale.x / baseTextureResolution, this.tileScale.y / baseTextureResolution); + + const anchorX = this.anchor.x * -this._width; + const anchorY = this.anchor.y * -this._height; + + if (this.uvRespectAnchor) + { + context.translate(modX, modY); + + context.fillRect(-modX + anchorX, -modY + anchorY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } + else + { + context.translate(modX + anchorX, modY + anchorY); + + context.fillRect(-modX, -modY, + this._width / this.tileScale.x * baseTextureResolution, + this._height / this.tileScale.y * baseTextureResolution); + } } /** diff --git a/src/extras/cacheAsBitmap.js b/src/extras/cacheAsBitmap.js index 18043a7..5dd5640 100644 --- a/src/extras/cacheAsBitmap.js +++ b/src/extras/cacheAsBitmap.js @@ -41,6 +41,9 @@ * provide a performance benefit for complex static displayObjects. * To remove simply set this property to 'false' * + * IMPORTANT GOTCHA - make sure that all your textures are preloaded BEFORE setting this property to true + * as it will take a snapshot of what is currently there. If the textures have not loaded then they will not appear. + * * @member {boolean} * @memberof PIXI.DisplayObject# */ diff --git a/src/extras/webgl/TilingSpriteRenderer.js b/src/extras/webgl/TilingSpriteRenderer.js index f8d0d92..2591719 100644 --- a/src/extras/webgl/TilingSpriteRenderer.js +++ b/src/extras/webgl/TilingSpriteRenderer.js @@ -13,7 +13,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class TilingSpriteRenderer extends core.ObjectRenderer { +export default class TilingSpriteRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/filters/displacement/DisplacementFilter.js b/src/filters/displacement/DisplacementFilter.js index 112de0b..d5355c3 100644 --- a/src/filters/displacement/DisplacementFilter.js +++ b/src/filters/displacement/DisplacementFilter.js @@ -36,7 +36,7 @@ this.maskMatrix = maskMatrix; this.uniforms.mapSampler = sprite.texture; - this.uniforms.filterMatrix = maskMatrix.toArray(true); + this.uniforms.filterMatrix = maskMatrix; this.uniforms.scale = { x: 1, y: 1 }; if (scale === null || scale === undefined) diff --git a/src/interaction/InteractionData.js b/src/interaction/InteractionData.js index 02d53a3..818da35 100644 --- a/src/interaction/InteractionData.js +++ b/src/interaction/InteractionData.js @@ -21,9 +21,9 @@ this.global = new core.Point(); /** - * The target Sprite that was interacted with + * The target DisplayObject that was interacted with * - * @member {PIXI.Sprite} + * @member {PIXI.DisplayObject} */ this.target = null; @@ -33,6 +33,13 @@ * @member {Event} */ this.originalEvent = null; + + /** + * Unique identifier for this interaction + * + * @member {number} + */ + this.identifier = null; } /** diff --git a/src/interaction/InteractionEvent.js b/src/interaction/InteractionEvent.js index 75b2966..dc554f9 100644 --- a/src/interaction/InteractionEvent.js +++ b/src/interaction/InteractionEvent.js @@ -58,7 +58,7 @@ } /** - * Prevents event from reaching any objects other than the current object. + * Resets the event. * * @private */ diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7d51134..5c9a045 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -1,6 +1,7 @@ import * as core from '../core'; import InteractionData from './InteractionData'; import InteractionEvent from './InteractionEvent'; +import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; import MobileDevice from 'ismobilejs'; @@ -11,8 +12,10 @@ interactiveTarget ); +const MOUSE_POINTER_ID = 'MOUSE'; + /** - * The interaction manager deals with mouse and touch events. Any DisplayObject can be interactive + * The interaction manager deals with mouse, touch and pointer events. Any DisplayObject can be interactive * if its interactive parameter is set to true * This manager also supports multitouch. * @@ -68,21 +71,28 @@ * @member {PIXI.interaction.InteractionData} */ this.mouse = new InteractionData(); + this.mouse.identifier = MOUSE_POINTER_ID; // setting the mouse to start off far off screen will mean that mouse over does // not get called before we even move the mouse. this.mouse.global.set(-999999); /** - * The pointer data + * Actively tracked InteractionData * - * @member {PIXI.interaction.InteractionData} + * @private + * @member {Object.} */ - this.pointer = new InteractionData(); + this.activeInteractionData = {}; + this.activeInteractionData[MOUSE_POINTER_ID] = this.mouse; - // setting the pointer to start off far off screen will mean that pointer over does - // not get called before we even move the pointer. - this.pointer.global.set(-999999); + /** + * Pool of unused InteractionData + * + * @private + * @member {PIXI.interation.InteractionData[]} + */ + this.interactionDataPool = []; /** * An event data object to handle all the event tracking/dispatching @@ -92,13 +102,6 @@ this.eventData = new InteractionEvent(); /** - * Tiny little interactiveData pool ! - * - * @member {PIXI.interaction.InteractionData[]} - */ - this.interactiveDataPool = []; - - /** * The DOM element to bind to. * * @private @@ -179,40 +182,6 @@ * @private * @member {Function} */ - this.onMouseUp = this.onMouseUp.bind(this); - this.processMouseUp = this.processMouseUp.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseDown = this.onMouseDown.bind(this); - this.processMouseDown = this.processMouseDown.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseMove = this.onMouseMove.bind(this); - this.processMouseMove = this.processMouseMove.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOut = this.onMouseOut.bind(this); - this.processMouseOverOut = this.processMouseOverOut.bind(this); - - /** - * @private - * @member {Function} - */ - this.onMouseOver = this.onMouseOver.bind(this); - - /** - * @private - * @member {Function} - */ this.onPointerUp = this.onPointerUp.bind(this); this.processPointerUp = this.processPointerUp.bind(this); @@ -220,6 +189,13 @@ * @private * @member {Function} */ + this.onPointerCancel = this.onPointerCancel.bind(this); + this.processPointerCancel = this.processPointerCancel.bind(this); + + /** + * @private + * @member {Function} + */ this.onPointerDown = this.onPointerDown.bind(this); this.processPointerDown = this.processPointerDown.bind(this); @@ -244,27 +220,6 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * @private - * @member {Function} - */ - this.onTouchStart = this.onTouchStart.bind(this); - this.processTouchStart = this.processTouchStart.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchEnd = this.onTouchEnd.bind(this); - this.processTouchEnd = this.processTouchEnd.bind(this); - - /** - * @private - * @member {Function} - */ - this.onTouchMove = this.onTouchMove.bind(this); - this.processTouchMove = this.processTouchMove.bind(this); - - /** * Every update cursor will be reset to this value, if some element wont override it in * its hitTest. * @@ -400,6 +355,13 @@ */ /** + * Fired when the operating system cancels a pointer event + * + * @event pointercancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a pointer device button is pressed and released on the display object. * * @event pointertap @@ -450,6 +412,13 @@ */ /** + * Fired when the operating system cancels a touch + * + * @event touchcancel + * @memberof PIXI.interaction.InteractionManager# + */ + + /** * Fired when a touch point is placed and removed from the display object. * * @event tap @@ -526,43 +495,25 @@ this.interactionDOMElement.addEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.addEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.addEventListener('pointerover', this.onPointerOver, true); + window.addEventListener('pointercancel', this.onPointerCancel, true); window.addEventListener('pointerup', this.onPointerUp, true); } + else { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) + window.document.addEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); + window.addEventListener('mouseup', this.onPointerUp, true); + + if (this.supportsTouchEvents) { this.interactionDOMElement.addEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.addEventListener('touchcancel', this.onPointerCancel, true); this.interactionDOMElement.addEventListener('touchend', this.onPointerUp, true); this.interactionDOMElement.addEventListener('touchmove', this.onPointerMove, true); } - - if (this.normalizeMouseEvents) - { - window.document.addEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onPointerOver, true); - window.addEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.addEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.addEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.addEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.addEventListener('mouseover', this.onMouseOver, true); - window.addEventListener('mouseup', this.onMouseUp, true); - - if (this.supportsTouchEvents) - { - this.interactionDOMElement.addEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.addEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.addEventListener('touchmove', this.onTouchMove, true); } this.eventsAdded = true; @@ -598,43 +549,22 @@ this.interactionDOMElement.removeEventListener('pointerdown', this.onPointerDown, true); this.interactionDOMElement.removeEventListener('pointerout', this.onPointerOut, true); this.interactionDOMElement.removeEventListener('pointerover', this.onPointerOver, true); + window.removeEventListener('pointercancel', this.onPointerCancel, true); window.removeEventListener('pointerup', this.onPointerUp, true); } - else - { - /** - * If pointer events aren't available on a device, this will turn either the touch or - * mouse events into pointer events. This allows a developer to just listen for emitted - * pointer events on interactive sprites - */ - if (this.normalizeTouchEvents) - { - this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); - } - if (this.normalizeMouseEvents) - { - window.document.removeEventListener('mousemove', this.onPointerMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); - window.removeEventListener('mouseup', this.onPointerUp, true); - } - } - - window.document.removeEventListener('mousemove', this.onMouseMove, true); - this.interactionDOMElement.removeEventListener('mousedown', this.onMouseDown, true); - this.interactionDOMElement.removeEventListener('mouseout', this.onMouseOut, true); - this.interactionDOMElement.removeEventListener('mouseover', this.onMouseOver, true); - window.removeEventListener('mouseup', this.onMouseUp, true); + window.document.removeEventListener('mousemove', this.onPointerMove, true); + this.interactionDOMElement.removeEventListener('mousedown', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('mouseout', this.onPointerOut, true); + this.interactionDOMElement.removeEventListener('mouseover', this.onPointerOver, true); + window.removeEventListener('mouseup', this.onPointerUp, true); if (this.supportsTouchEvents) { - this.interactionDOMElement.removeEventListener('touchstart', this.onTouchStart, true); - this.interactionDOMElement.removeEventListener('touchend', this.onTouchEnd, true); - this.interactionDOMElement.removeEventListener('touchmove', this.onTouchMove, true); + this.interactionDOMElement.removeEventListener('touchstart', this.onPointerDown, true); + this.interactionDOMElement.removeEventListener('touchcancel', this.onPointerCancel, true); + this.interactionDOMElement.removeEventListener('touchend', this.onPointerUp, true); + this.interactionDOMElement.removeEventListener('touchmove', this.onPointerMove, true); } this.interactionDOMElement = null; @@ -664,7 +594,7 @@ return; } - // if the user move the mouse this check has already been dfone using the mouse move! + // if the user move the mouse this check has already been done using the mouse move! if (this.didMove) { this.didMove = false; @@ -677,9 +607,30 @@ // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. // In this case, mouseover and mouseevents would not pass the flag test in dispatchEvent function - this.eventData._reset(); + for (const k in this.activeInteractionData) + { + // eslint-disable-next-line no-prototype-builtins + if (this.activeInteractionData.hasOwnProperty(k)) + { + const interactionData = this.activeInteractionData[k]; - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, true); + if (interactionData.originalEvent && interactionData.pointerType !== 'touch') + { + const interactionEvent = this.configureInteractionEventForDOMEvent( + this.eventData, + interactionData.originalEvent, + interactionData + ); + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerOverOut, + true + ); + } + } + } if (this.currentCursorStyle !== this.cursor) { @@ -748,22 +699,26 @@ * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * - * @param {PIXI.Point} point - the point that is tested for collision + * @private + * @param {InteractionEvent} interactionEvent - event containing the point that + * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The - * displayObject and hit will be passed to the function + * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ - processInteractive(point, displayObject, func, hitTest, interactive) + processInteractive(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } + const point = interactionEvent.data.global; + // Took a little while to rework this function correctly! But now it is done and nice and optimised. ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS @@ -818,7 +773,7 @@ const child = children[i]; // time to get recursive.. if this function will return if something is hit.. - if (this.processInteractive(point, child, func, hitTest, interactiveParent)) + if (this.processInteractive(interactionEvent, child, func, hitTest, interactiveParent)) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best @@ -868,14 +823,12 @@ if (displayObject.interactive) { - if (hit && !this.eventData.target) + if (hit && !interactionEvent.target) { - this.eventData.target = displayObject; - this.mouse.target = displayObject; - this.pointer.target = displayObject; + interactionEvent.target = displayObject; } - func(displayObject, hit); + func(interactionEvent, displayObject, hit); } } @@ -883,271 +836,172 @@ } /** - * Is called when the mouse button is pressed down on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being pressed down - */ - onMouseDown(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - if (this.autoPreventDefault) - { - this.mouse.originalEvent.preventDefault(); - } - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseDown, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - - /** - * Processes the result of the mouse down check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseDown(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - - if (hit) - { - displayObject[isRightButton ? '_isRightDown' : '_isLeftDown'] = true; - this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', this.eventData); - } - } - - /** - * Is called when the mouse button is released on the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of a mouse button being released - */ - onMouseUp(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseUp, true); - - const isRightButton = event.button === 2 || event.which === 3; - - this.emit(isRightButton ? 'rightup' : 'mouseup', this.eventData); - } - - /** - * Processes the result of the mouse up check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseUp(displayObject, hit) - { - const e = this.mouse.originalEvent; - - const isRightButton = e.button === 2 || e.which === 3; - const isDown = isRightButton ? '_isRightDown' : '_isLeftDown'; - - if (hit) - { - this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', this.eventData); - - if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', this.eventData); - } - } - else if (displayObject[isDown]) - { - displayObject[isDown] = false; - this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', this.eventData); - } - } - - /** - * Is called when the mouse moves across the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving - */ - onMouseMove(event) - { - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.didMove = true; - - this.cursor = this.defaultCursorStyle; - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseMove, true); - - this.emit('mousemove', this.eventData); - - if (this.currentCursorStyle !== this.cursor) - { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; - } - - // TODO BUG for parents interactive object (border order issue) - } - - /** - * Processes the result of the mouse move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseMove(displayObject, hit) - { - this.processMouseOverOut(displayObject, hit); - - // only display on mouse over - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'mousemove', this.eventData); - } - } - - /** - * Is called when the mouse is moved out of the renderer element - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse being moved out - */ - onMouseOut(event) - { - this.mouseOverRenderer = false; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - // Update internal mouse reference - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; - - // TODO optimize by not check EVERY TIME! maybe half as often? // - this.mapPositionToPoint(this.mouse.global, event.clientX, event.clientY); - - this.processInteractive(this.mouse.global, this.renderer._lastObjectRendered, this.processMouseOverOut, false); - - this.emit('mouseout', this.eventData); - } - - /** - * Processes the result of the mouse over/out check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processMouseOverOut(displayObject, hit) - { - if (hit && this.mouseOverRenderer) - { - if (!displayObject._mouseOver) - { - displayObject._mouseOver = true; - this.dispatchEvent(displayObject, 'mouseover', this.eventData); - } - - if (displayObject.buttonMode) - { - this.cursor = displayObject.defaultCursor; - } - } - else if (displayObject._mouseOver) - { - displayObject._mouseOver = false; - this.dispatchEvent(displayObject, 'mouseout', this.eventData); - } - } - - /** - * Is called when the mouse enters the renderer element area - * - * @private - * @param {MouseEvent} event - The DOM event of the mouse moving into the renderer view - */ - onMouseOver(event) - { - this.mouseOverRenderer = true; - - this.mouse.originalEvent = event; - this.eventData.data = this.mouse; - this.eventData._reset(); - - this.emit('mouseover', this.eventData); - } - - /** * Is called when the pointer button is pressed down on the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being pressed down + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being pressed down */ - onPointerDown(event) + onPointerDown(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + const events = this.normalizeToPointerData(originalEvent); /** * No need to prevent default on natural pointer events, as there are no side effects * Normalized events, however, may have the double mousedown/touchstart issue on the native android browser, * so still need to be prevented. */ - if (this.autoPreventDefault && (this.normalizeMouseEvents || this.normalizeTouchEvents)) + + // Guaranteed that there will be at least one event in events, and all events must have the same pointer type + + if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) { - this.pointer.originalEvent.preventDefault(); + originalEvent.preventDefault(); } - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerDown, true); + const eventLen = events.length; - this.emit('pointerdown', this.eventData); + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerDown, true); + + this.emit('pointerdown', interactionEvent); + if (event.pointerType === 'touch') + { + this.emit('touchstart', interactionEvent); + } + else if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightdown' : 'mousedown', this.eventData); + } + } } /** * Processes the result of the pointer down check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerDown(displayObject, hit) + processPointerDown(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + if (hit) { - displayObject._pointerDown = true; - this.dispatchEvent(displayObject, 'pointerdown', this.eventData); + displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); + + if (e.type === 'touchstart' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchstart', interactionEvent); + } + else if (e.type === 'mousedown' || e.pointerType === 'mouse') + { + const isRightButton = e.button === 2 || e.which === 3; + + if (isRightButton) + { + displayObject.getTrackedPointers()[id].rightDown = true; + } + else + { + displayObject.getTrackedPointers()[id].leftDown = true; + } + + this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); + } + } + } + + /** + * Is called when the pointer button is released on the renderer element + * + * @private + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being released + * @param {boolean} cancelled - true if the pointer is cancelled + * @param {Function} func - Function passed to {@link processInteractive} + */ + onPointerComplete(originalEvent, cancelled, func) + { + const events = this.normalizeToPointerData(originalEvent); + + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, func, true); + + this.emit(cancelled ? 'pointercancel' : 'pointerup', interactionEvent); + + if (event.pointerType === 'mouse') + { + const isRightButton = event.button === 2 || event.which === 3; + + this.emit(isRightButton ? 'rightup' : 'mouseup', interactionEvent); + } + else if (event.pointerType === 'touch') + { + this.emit(cancelled ? 'touchcancel' : 'touchend', interactionEvent); + this.releaseInteractionDataForPointerId(event.pointerId, interactionData); + } + } + } + + /** + * Is called when the pointer button is cancelled + * + * @private + * @param {PointerEvent} event - The DOM event of a pointer button being released + */ + onPointerCancel(event) + { + this.onPointerComplete(event, true, this.processPointerCancel); + } + + /** + * Processes the result of the pointer cancel check and dispatches the event if need be + * + * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event + * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested + */ + processPointerCancel(interactionEvent, displayObject) + { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + if (displayObject.getTrackedPointers()[id] !== undefined) + { + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); + + if (e.type === 'touchcancel' || e.pointerType === 'touch') + { + this.dispatchEvent(displayObject, 'touchcancel', interactionEvent); + } } } @@ -1159,42 +1013,73 @@ */ onPointerUp(event) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); - - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); - - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerUp, true); - - this.emit('pointerup', this.eventData); + this.onPointerComplete(event, false, this.processPointerUp); } /** * Processes the result of the pointer up check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerUp(displayObject, hit) + processPointerUp(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const trackingData = displayObject.getTrackedPointers()[id]; + + const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); + + const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); + + // Pointers and Touches if (hit) { - this.dispatchEvent(displayObject, 'pointerup', this.eventData); + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - if (displayObject._pointerDown) + if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointertap', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); } } - else if (displayObject._pointerDown) + else if (displayObject.getTrackedPointers()[id] !== undefined) { - displayObject._pointerDown = false; - this.dispatchEvent(displayObject, 'pointerupoutside', this.eventData); + delete displayObject.getTrackedPointers()[id]; + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + + // Mouse only + if (isMouse) + { + const isRightButton = e.button === 2 || e.which === 3; + + const flags = InteractionTrackingData.FLAGS; + + const test = isRightButton ? flags.RIGHT_DOWN : flags.LEFT_DOWN; + + const isDown = trackingData !== undefined && (trackingData.flags | test); + + if (hit) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightup' : 'mouseup', interactionEvent); + + if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightclick' : 'click', interactionEvent); + } + } + else if (isDown) + { + this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); + } } } @@ -1202,39 +1087,82 @@ * Is called when the pointer moves across the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer moving + * @param {PointerEvent} originalEvent - The DOM event of a pointer moving */ - onPointerMove(event) + onPointerMove(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + if (events[0].pointerType === 'mouse') + { + this.didMove = true; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerMove, true); + this.cursor = this.defaultCursorStyle; + } - this.emit('pointermove', this.eventData); + const eventLen = events.length; + + for (let i = 0; i < eventLen; i++) + { + const event = events[i]; + + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = originalEvent; + + const interactive = event.pointerType === 'touch' ? this.moveWhenInside : true; + + this.processInteractive( + interactionEvent, + this.renderer._lastObjectRendered, + this.processPointerMove, + interactive + ); + this.emit('pointermove', interactionEvent); + if (event.pointerType === 'touch') this.emit('touchmove', interactionEvent); + if (event.pointerType === 'mouse') this.emit('mousemove', interactionEvent); + } + + if (events[0].pointerType === 'mouse') + { + if (this.currentCursorStyle !== this.cursor) + { + this.currentCursorStyle = this.cursor; + this.interactionDOMElement.style.cursor = this.cursor; + } + + // TODO BUG for parents interactive object (border order issue) + } } /** * Processes the result of the pointer move check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerMove(displayObject, hit) + processPointerMove(interactionEvent, displayObject, hit) { - if (!this.pointer.originalEvent.changedTouches) + const e = interactionEvent.data.originalEvent; + + const isTouch = (e.type === 'touchmove' || e.pointerType === 'touch'); + + const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); + + if (e.type !== 'touchmove') { - this.processPointerOverOut(displayObject, hit); + this.processPointerOverOut(interactionEvent, displayObject, hit); } if (!this.moveWhenInside || hit) { - this.dispatchEvent(displayObject, 'pointermove', this.eventData); + this.dispatchEvent(displayObject, 'pointermove', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchmove', interactionEvent); + if (isMouse) this.dispatchEvent(displayObject, 'mousemove', interactionEvent); } } @@ -1242,44 +1170,81 @@ * Is called when the pointer is moved out of the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer being moved out + * @param {PointerEvent} originalEvent - The DOM event of a pointer being moved out */ - onPointerOut(event) + onPointerOut(originalEvent) { - this.normalizeToPointerData(event); - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - // Update internal pointer reference - this.mapPositionToPoint(this.pointer.global, event.clientX, event.clientY); + // Only mouse and pointer can call onPointerOut, so events will always be length 1 + const event = events[0]; - this.processInteractive(this.pointer.global, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + if (event.pointerType === 'mouse') + { + this.mouseOverRenderer = false; + this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + } - this.emit('pointerout', this.eventData); + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + this.processInteractive(interactionEvent, this.renderer._lastObjectRendered, this.processPointerOverOut, false); + + this.emit('pointerout', interactionEvent); + if (event.pointerType === 'mouse') + { + this.emit('mouseout', interactionEvent); + } } /** * Processes the result of the pointer over/out check and dispatches the event if need be * * @private + * @param {InteractionEvent} interactionEvent - The interaction event wrapping the DOM event * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested * @param {boolean} hit - the result of the hit test on the display object */ - processPointerOverOut(displayObject, hit) + processPointerOverOut(interactionEvent, displayObject, hit) { + const e = interactionEvent.data.originalEvent; + + const id = interactionEvent.data.identifier; + + const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); + + const trackingData = displayObject.getTrackedPointers()[id]; + + if (trackingData === undefined) return; + if (hit && this.mouseOverRenderer) { - if (!displayObject._pointerOver) + if (!trackingData.over) { - displayObject._pointerOver = true; - this.dispatchEvent(displayObject, 'pointerover', this.eventData); + trackingData.over = true; + this.dispatchEvent(displayObject, 'pointerover', interactionEvent); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseover', interactionEvent); + } + } + + if (isMouse && displayObject.buttonMode) + { + this.cursor = displayObject.defaultCursor; } } - else if (displayObject._pointerOver) + else if (trackingData.over) { - displayObject._pointerOver = false; + trackingData.over = false; this.dispatchEvent(displayObject, 'pointerout', this.eventData); + if (isMouse) + { + this.dispatchEvent(displayObject, 'mouseout', interactionEvent); + } } } @@ -1287,253 +1252,150 @@ * Is called when the pointer is moved into the renderer element * * @private - * @param {PointerEvent} event - The DOM event of a pointer button being moved into the renderer view + * @param {PointerEvent} originalEvent - The DOM event of a pointer button being moved into the renderer view */ - onPointerOver(event) + onPointerOver(originalEvent) { - this.pointer.originalEvent = event; - this.eventData.data = this.pointer; - this.eventData._reset(); + const events = this.normalizeToPointerData(originalEvent); - this.emit('pointerover', this.eventData); - } + // Only mouse and pointer can call onPointerOver, so events will always be length 1 + const event = events[0]; - /** - * Is called when a touch is started on the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch starting on the renderer view - */ - onTouchStart(event) - { - if (this.autoPreventDefault) + const interactionData = this.getInteractionDataForPointerId(event.pointerId); + + const interactionEvent = this.configureInteractionEventForDOMEvent(this.eventData, event, interactionData); + + interactionEvent.data.originalEvent = event; + + if (event.pointerType === 'mouse') { - event.preventDefault(); + this.mouseOverRenderer = true; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + this.emit('pointerover', interactionEvent); + if (event.pointerType === 'mouse') { - const touch = changedTouches[i]; - const touchData = this.getTouchData(touch); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchStart, true); - - this.emit('touchstart', this.eventData); - - this.returnTouchData(touchData); + this.emit('mouseover', interactionEvent); } } /** - * Processes the result of a touch check and dispatches the event if need be + * Get InteractionData for a given pointerId. Store that data as well * * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object + * @param {number} pointerId - Identifier from a pointer event + * @return {InteractionData} - Interaction data for the given pointer identifier */ - processTouchStart(displayObject, hit) + getInteractionDataForPointerId(pointerId) { - if (hit) + if (pointerId === MOUSE_POINTER_ID) { - displayObject._touchDown = true; - this.dispatchEvent(displayObject, 'touchstart', this.eventData); + return this.mouse; + } + else if (this.activeInteractionData[pointerId]) + { + return this.activeInteractionData[pointerId]; + } + + const interactionData = this.interactionDataPool.pop() || new InteractionData(); + + interactionData.identifier = pointerId; + this.activeInteractionData[pointerId] = interactionData; + + return interactionData; + } + + /** + * Return unused InteractionData to the pool, for a given pointerId + * + * @private + * @param {number} pointerId - Identifier from a pointer event + */ + releaseInteractionDataForPointerId(pointerId) + { + const interactionData = this.activeInteractionData[pointerId]; + + if (interactionData) + { + delete this.activeInteractionData[pointerId]; + this.interactionDataPool.push(interactionData); } } /** - * Is called when a touch ends on the renderer element + * Configure an InteractionEvent to wrap a DOM PointerEvent and InteractionData * * @private - * @param {TouchEvent} event - The DOM event of a touch ending on the renderer view + * @param {InteractionEvent} interactionEvent - The event to be configured + * @param {PointerEvent} pointerEvent - The DOM event that will be paired with the InteractionEvent + * @param {InteractionData} interactionData - The InteractionData that will be paired with the InteractionEvent + * @return {InteractionEvent} the interaction event that was passed in */ - onTouchEnd(event) + configureInteractionEventForDOMEvent(interactionEvent, pointerEvent, interactionData) { - if (this.autoPreventDefault) + interactionEvent.data = interactionData; + + this.mapPositionToPoint(interactionData.global, pointerEvent.clientX, pointerEvent.clientY); + + // This is the way InteractionManager processed touch events before the refactoring, so I've kept + // it here. But it doesn't make that much sense to me, since mapPositionToPoint already factors + // in this.resolution, so this just divides by this.resolution twice for touch events... + if (navigator.isCocoonJS && event.pointerType === 'touch') { - event.preventDefault(); + interactionData.global.x = interactionData.global.x / this.resolution; + interactionData.global.y = interactionData.global.y / this.resolution; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; - - for (let i = 0; i < cLength; i++) + // Not really sure why this is happening, but it's how a previous version handled things + if (pointerEvent.pointerType === 'touch') { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - // TODO this should be passed along.. no set - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive(touchData.global, this.renderer._lastObjectRendered, this.processTouchEnd, true); - - this.emit('touchend', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of the end of a touch and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchEnd(displayObject, hit) - { - if (hit) - { - this.dispatchEvent(displayObject, 'touchend', this.eventData); - - if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'tap', this.eventData); - } - } - else if (displayObject._touchDown) - { - displayObject._touchDown = false; - this.dispatchEvent(displayObject, 'touchendoutside', this.eventData); - } - } - - /** - * Is called when a touch is moved across the renderer element - * - * @private - * @param {TouchEvent} event - The DOM event of a touch moving accross the renderer view - */ - onTouchMove(event) - { - if (this.autoPreventDefault) - { - event.preventDefault(); + pointerEvent.globalX = interactionData.global.x; + pointerEvent.globalY = interactionData.global.y; } - const changedTouches = event.changedTouches; - const cLength = changedTouches.length; + interactionData.originalEvent = pointerEvent; + interactionEvent._reset(); - for (let i = 0; i < cLength; i++) - { - const touchEvent = changedTouches[i]; - - const touchData = this.getTouchData(touchEvent); - - touchData.originalEvent = event; - - this.eventData.data = touchData; - this.eventData._reset(); - - this.processInteractive( - touchData.global, - this.renderer._lastObjectRendered, - this.processTouchMove, - this.moveWhenInside - ); - - this.emit('touchmove', this.eventData); - - this.returnTouchData(touchData); - } - } - - /** - * Processes the result of a touch move check and dispatches the event if need be - * - * @private - * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - The display object that was tested - * @param {boolean} hit - the result of the hit test on the display object - */ - processTouchMove(displayObject, hit) - { - if (!this.moveWhenInside || hit) - { - this.dispatchEvent(displayObject, 'touchmove', this.eventData); - } - } - - /** - * Grabs an interaction data object from the internal pool - * - * @private - * @param {Touch} touch - The touch data we need to pair with an interactionData object - * @return {PIXI.interaction.InteractionData} The built data object. - */ - getTouchData(touch) - { - const touchData = this.interactiveDataPool.pop() || new InteractionData(); - - touchData.identifier = touch.identifier; - this.mapPositionToPoint(touchData.global, touch.clientX, touch.clientY); - - if (navigator.isCocoonJS) - { - touchData.global.x = touchData.global.x / this.resolution; - touchData.global.y = touchData.global.y / this.resolution; - } - - touch.globalX = touchData.global.x; - touch.globalY = touchData.global.y; - - return touchData; - } - - /** - * Returns an interaction data object to the internal pool - * - * @private - * @param {PIXI.interaction.InteractionData} touchData - The touch data object we want to return to the pool - */ - returnTouchData(touchData) - { - this.interactiveDataPool.push(touchData); + return interactionEvent; } /** * Ensures that the original event object contains all data that a regular pointer event would have * * @private - * @param {TouchEvent|MouseEvent} event - The original event data from a touch or mouse event + * @param {TouchEvent|MouseEvent|PointerEvent} event - The original event data from a touch or mouse event + * @return {PointerEvent[]} An array containing a single normalized pointer event, in the case of a pointer + * or mouse event, or a multiple normalized pointer events if there are multiple changed touches */ normalizeToPointerData(event) { - if (this.normalizeTouchEvents && event.changedTouches) - { - if (typeof event.button === 'undefined') event.button = event.touches.length ? 1 : 0; - if (typeof event.buttons === 'undefined') event.buttons = event.touches.length ? 1 : 0; - if (typeof event.isPrimary === 'undefined') event.isPrimary = event.touches.length === 1; - if (typeof event.width === 'undefined') event.width = event.changedTouches[0].radiusX || 1; - if (typeof event.height === 'undefined') event.height = event.changedTouches[0].radiusY || 1; - if (typeof event.tiltX === 'undefined') event.tiltX = 0; - if (typeof event.tiltY === 'undefined') event.tiltY = 0; - if (typeof event.pointerType === 'undefined') event.pointerType = 'touch'; - if (typeof event.pointerId === 'undefined') event.pointerId = event.changedTouches[0].identifier || 0; - if (typeof event.pressure === 'undefined') event.pressure = event.changedTouches[0].force || 0.5; - if (typeof event.rotation === 'undefined') event.rotation = event.changedTouches[0].rotationAngle || 0; + const normalizedEvents = []; - if (typeof event.clientX === 'undefined') event.clientX = event.changedTouches[0].clientX; - if (typeof event.clientY === 'undefined') event.clientY = event.changedTouches[0].clientY; - if (typeof event.pageX === 'undefined') event.pageX = event.changedTouches[0].pageX; - if (typeof event.pageY === 'undefined') event.pageY = event.changedTouches[0].pageY; - if (typeof event.screenX === 'undefined') event.screenX = event.changedTouches[0].screenX; - if (typeof event.screenY === 'undefined') event.screenY = event.changedTouches[0].screenY; - if (typeof event.layerX === 'undefined') event.layerX = event.offsetX = event.clientX; - if (typeof event.layerY === 'undefined') event.layerY = event.offsetY = event.clientY; + if (this.supportsTouchEvents && event instanceof TouchEvent) + { + for (let i = 0, li = event.changedTouches.length; i < li; i++) + { + const touch = event.changedTouches[i]; + + if (typeof touch.button === 'undefined') touch.button = event.touches.length ? 1 : 0; + if (typeof touch.buttons === 'undefined') touch.buttons = event.touches.length ? 1 : 0; + if (typeof touch.isPrimary === 'undefined') touch.isPrimary = event.touches.length === 1; + if (typeof touch.width === 'undefined') touch.width = touch.radiusX || 1; + if (typeof touch.height === 'undefined') touch.height = touch.radiusY || 1; + if (typeof touch.tiltX === 'undefined') touch.tiltX = 0; + if (typeof touch.tiltY === 'undefined') touch.tiltY = 0; + if (typeof touch.pointerType === 'undefined') touch.pointerType = 'touch'; + if (typeof touch.pointerId === 'undefined') touch.pointerId = touch.identifier || 0; + if (typeof touch.pressure === 'undefined') touch.pressure = touch.force || 0.5; + if (typeof touch.rotation === 'undefined') touch.rotation = touch.rotationAngle || 0; + + if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; + if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + + normalizedEvents.push(touch); + } } - else if (this.normalizeMouseEvents) + else if (event instanceof MouseEvent) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1541,10 +1403,18 @@ if (typeof event.tiltX === 'undefined') event.tiltX = 0; if (typeof event.tiltY === 'undefined') event.tiltY = 0; if (typeof event.pointerType === 'undefined') event.pointerType = 'mouse'; - if (typeof event.pointerId === 'undefined') event.pointerId = 1; + if (typeof event.pointerId === 'undefined') event.pointerId = MOUSE_POINTER_ID; if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + + normalizedEvents.push(event); } + else + { + normalizedEvents.push(event); + } + + return normalizedEvents; } /** @@ -1563,30 +1433,17 @@ this.eventData = null; - this.interactiveDataPool = null; - this.interactionDOMElement = null; - this.onMouseDown = null; - this.processMouseDown = null; - - this.onMouseUp = null; - this.processMouseUp = null; - - this.onMouseMove = null; - this.processMouseMove = null; - - this.onMouseOut = null; - this.processMouseOverOut = null; - - this.onMouseOver = null; - this.onPointerDown = null; this.processPointerDown = null; this.onPointerUp = null; this.processPointerUp = null; + this.onPointerCancel = null; + this.processPointerCancel = null; + this.onPointerMove = null; this.processPointerMove = null; @@ -1595,15 +1452,6 @@ this.onPointerOver = null; - this.onTouchStart = null; - this.processTouchStart = null; - - this.onTouchEnd = null; - this.processTouchEnd = null; - - this.onTouchMove = null; - this.processTouchMove = null; - this._tempPoint = null; } } diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js new file mode 100644 index 0000000..3ce38b6 --- /dev/null +++ b/src/interaction/InteractionTrackingData.js @@ -0,0 +1,136 @@ +/** + * DisplayObjects with the {@link PIXI.interaction.interactiveTarget} mixin use this class to track interactions + * + * @class + * @private + * @memberof PIXI.interaction + */ +export default class InteractionTrackingData +{ + /** + * @param {number} pointerId - Unique pointer id of the event + */ + constructor(pointerId) + { + this._pointerId = pointerId; + this._flags = InteractionTrackingData.FLAGS.NONE; + } + + /** + * + * @private + * @param {number} flag - The interaction flag to set + * @param {boolean} yn - Should the flag be set or unset + */ + _doSet(flag, yn) + { + if (yn) + { + this._flags = this._flags | flag; + } + else + { + this._flags = this._flags & (~flag); + } + } + + /** + * @readonly + * @type {number} Unique pointer id of the event + */ + get pointerId() + { + return this._pointerId; + } + + /** + * State of the tracking data, expressed as bit flags + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get flags() + { + return this._flags; + } + + /** + * Set the flags for the tracking data + * + * @param {number} flags - Flags to set + */ + set flags(flags) + { + this._flags = flags; + } + + /** + * Is the tracked event over the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get over() + { + return (this._flags | this.constructor.FLAGS.OVER) !== 0; + } + + /** + * Set the over flag + * + * @param {boolean} yn - Is the event over? + */ + set over(yn) + { + this._doSet(this.constructor.FLAGS.OVER, yn); + } + + /** + * Did the right mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get rightDown() + { + return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + } + + /** + * Set the right down flag + * + * @param {boolean} yn - Is the right mouse button down? + */ + set rightDown(yn) + { + this._doSet(this.constructor.FLAGS.RIGHT_DOWN, yn); + } + + /** + * Did the left mouse button come down in the DisplayObject? + * + * @member {boolean} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get leftDown() + { + return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + } + + /** + * Set the left down flag + * + * @param {boolean} yn - Is the left mouse button down? + */ + set leftDown(yn) + { + this._doSet(this.constructor.FLAGS.LEFT_DOWN, yn); + } +} + +InteractionTrackingData.FLAGS = Object.freeze({ + NONE: 0, + OVER: 1 << 0, + LEFT_DOWN: 1 << 1, + RIGHT_DOWN: 1 << 2, +}); diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index 5a7e678..ebfaacc 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -9,7 +9,7 @@ * function MyObject() {} * * Object.assign( - * MyObject.prototype, + * core.DisplayObject.prototype, * PIXI.interaction.interactiveTarget * ); */ @@ -54,52 +54,16 @@ */ defaultCursor: 'pointer', - // some internal checks.. /** - * Internal check to detect if the mouse cursor is hovered over the displayObject + * Internal set of all active pointers, by identifier * - * @inner {boolean} + * @returns {Map} Map of all tracked pointers, by identifier * @private */ - _over: false, + getTrackedPointers: function getTrackedPointers() + { + if (this._trackedPointers === undefined) this._trackedPointers = {}; - /** - * Internal check to detect if the left mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isLeftDown: false, - - /** - * Internal check to detect if the right mouse button is pressed on the displayObject - * - * @inner {boolean} - * @private - */ - _isRightDown: false, - - /** - * Internal check to detect if the pointer cursor is hovered over the displayObject - * - * @inner {boolean} - * @private - */ - _pointerOver: false, - - /** - * Internal check to detect if the pointer is down on the displayObject - * - * @inner {boolean} - * @private - */ - _pointerDown: false, - - /** - * Internal check to detect if a user has touched the displayObject - * - * @inner {boolean} - * @private - */ - _touchDown: false, + return this._trackedPointers; + }, }; diff --git a/src/mesh/webgl/MeshRenderer.js b/src/mesh/webgl/MeshRenderer.js index e1d88e7..8e61919 100644 --- a/src/mesh/webgl/MeshRenderer.js +++ b/src/mesh/webgl/MeshRenderer.js @@ -11,7 +11,8 @@ * @memberof PIXI * @extends PIXI.ObjectRenderer */ -export default class MeshRenderer extends core.ObjectRenderer { +export default class MeshRenderer extends core.ObjectRenderer +{ /** * constructor for renderer diff --git a/src/prepare/limiters/CountLimiter.js b/src/prepare/limiters/CountLimiter.js index 7fd0b70..265c46c 100644 --- a/src/prepare/limiters/CountLimiter.js +++ b/src/prepare/limiters/CountLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class CountLimiter { +export default class CountLimiter +{ /** * @param {number} maxItemsPerFrame - The maximum number of items that can be prepared each frame. */ diff --git a/src/prepare/limiters/TimeLimiter.js b/src/prepare/limiters/TimeLimiter.js index 8908aba..5f40686 100644 --- a/src/prepare/limiters/TimeLimiter.js +++ b/src/prepare/limiters/TimeLimiter.js @@ -5,7 +5,8 @@ * @class * @memberof PIXI */ -export default class TimeLimiter { +export default class TimeLimiter +{ /** * @param {number} maxMilliseconds - The maximum milliseconds that can be spent preparing items each frame. */ diff --git a/test/core/Text.js b/test/core/Text.js index b8ce561..5799ad4 100644 --- a/test/core/Text.js +++ b/test/core/Text.js @@ -112,4 +112,42 @@ expect(text.width).to.equal(300); }); }); + + describe('text', function () + { + it('should convert numbers into strings', function () + { + const text = new PIXI.Text(2); + + expect(text.text).to.equal('2'); + }); + + it('should not change 0 to \'\'', function () + { + const text = new PIXI.Text(0); + + expect(text.text).to.equal('0'); + }); + + it('should prevent setting null', function () + { + const text = new PIXI.Text(null); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting undefined', function () + { + const text = new PIXI.Text(); + + expect(text.text).to.equal(' '); + }); + + it('should prevent setting \'\'', function () + { + const text = new PIXI.Text(''); + + expect(text.text).to.equal(' '); + }); + }); }); diff --git a/test/core/getLocalBounds.js b/test/core/getLocalBounds.js index d160a56..309da0e 100644 --- a/test/core/getLocalBounds.js +++ b/test/core/getLocalBounds.js @@ -196,4 +196,13 @@ expect(bounds.width).to.equal(100); expect(bounds.height).to.equal(100); }); + + it('should register correct local-bounds with a Text', function () + { + const text = new PIXI.Text('hello'); + const bounds = text.getLocalBounds(); + + expect(bounds.width).to.not.equal(0); + expect(bounds.height).to.not.equal(0); + }); }); diff --git a/test/interaction/MockPointer.js b/test/interaction/MockPointer.js index 5bac0d3..7017656 100644 --- a/test/interaction/MockPointer.js +++ b/test/interaction/MockPointer.js @@ -5,7 +5,8 @@ * * @class */ -class MockPointer { +class MockPointer +{ /** * @param {PIXI.Container} stage - The root of the scene tree * @param {number} [width=100] - Width of the renderer @@ -57,9 +58,15 @@ */ mousedown(x, y) { + const mouseEvent = new MouseEvent('mousedown', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + this.setPosition(x, y); this.render(); - this.interaction.onMouseDown({ clientX: 0, clientY: 0, preventDefault: sinon.stub() }); + this.interaction.onPointerDown(mouseEvent); } /** @@ -68,9 +75,15 @@ */ mouseup(x, y) { + const mouseEvent = new MouseEvent('mouseup', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + this.setPosition(x, y); this.render(); - this.interaction.onMouseUp({ clientX: 0, clientY: 0, preventDefault: sinon.stub() }); + this.interaction.onPointerUp(mouseEvent); } /** @@ -89,12 +102,16 @@ */ touchstart(x, y) { + const touchEvent = new TouchEvent('touchstart', { + preventDefault: sinon.stub(), + changedTouches: [ + new Touch({ identifier: 0, target: this.renderer.view }), + ], + }); + this.setPosition(x, y); this.render(); - this.interaction.onTouchStart({ - preventDefault: sinon.stub(), - changedTouches: [new Touch({ identifier: 0, target: this.renderer.view })], - }); + this.interaction.onPointerDown(touchEvent); } /** @@ -103,12 +120,16 @@ */ touchend(x, y) { + const touchEvent = new TouchEvent('touchend', { + preventDefault: sinon.stub(), + changedTouches: [ + new Touch({ identifier: 0, target: this.renderer.view }), + ], + }); + this.setPosition(x, y); this.render(); - this.interaction.onTouchEnd({ - preventDefault: sinon.stub(), - changedTouches: [new Touch({ identifier: 0, target: this.renderer.view })], - }); + this.interaction.onPointerUp(touchEvent); } }